Go 语言入门指南:基础语法(二) | 青训营

86 阅读11分钟

1. 指针

1.1 指针的概念

区别于C/C++中的指针,Go语言中的指针不能进行偏移和运算,是安全指针。

Go 语言中的指针是一个变量,它存储另一个变量的内存地址。这样就允许直接访问和修改该内存位置的变量值。要在 GoLang 中声明一个指针,您可以在变量名前使用 * 符号。

例如,让我们声明一个变量 x 和一个指向它的指针 p

var x int = 19
var p *int = &x

在这段代码中,我们声明了一个整数变量 x 并将其值设置为 10。然后我们使用 & 运算符取出变量x的地址,同时赋值给了一个*int类型的指针 p

要访问存储在指针内存地址中的变量的值,请再次使用 * 符号。例如:

fmt.Println(*p) // prints 19

1.2 指针的用法

  • 在函数之间传递大型数据结构

在 GoLang 中,默认情况下所有函数参数都是按值传递的,这意味着每次调用函数时都会制作数据结构的副本。如果数据结构很大,这可能是一个性能问题。

使用指向数据结构的指针允许您传递数据结构的内存地址而不是进行复制。这可以更有效,尤其是在数据结构非常大的情况下。

  • 修改函数内变量的值

在 Go 语言中,所有的函数参数都是按值传递的,这意味着如果你修改函数内部变量的值,函数外部的原始变量不会受到影响。

使用指向变量的指针允许您直接修改内存地址处的值,这会在函数外部更改原始变量的值

func modify1(x int) {
    x = 100
}

func modify2(x *int) {
    *x = 100
}

func main() {
    a := 10
    modify1(a)
    fmt.Println(a) // 10
    modify2(&a)
    fmt.Println(a) // 100
}
  • 动态分配内存

在 Go 语言中,您可以使用 new() 和make()函数为变量分配内存。 

new()是一个内置的函数,它的函数签名如下:

    func new(Type) *Type
    1.Type表示类型,new函数只接受一个参数,这个参数是一个类型
    2.*Type表示类型指针,new函数返回一个指向该类型内存地址的指针。

使用new函数得到的是一个类型的指针,并且该指针对应的值为该类型的零值。举个例子:

func main() {
    a := new(int)
    b := new(bool)
    fmt.Printf("%T\n", a) // *int
    fmt.Printf("%T\n", b) // *bool
    fmt.Println(*a)       // 0
    fmt.Println(*b)       // false
}

make()也是内置函数,用于内存分配的,区别于new(),它只用于slice、map以及chan的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了。make函数的函数签名如下:

func make(t Type, size ...IntegerType) Type

make函数是无可替代的,我们在使用slice、map以及channel的时候,都需要使用make进行初始化,然后才可以对它们进行操作。

func main() {
    var b map[string]int
    b = make(map[string]int, 10)
    b["测试"] = 100
    fmt.Println(b)
}
var b map[string]int只是声明变量b是一个map类型的变量,需要使用make函数进行初始化操作之后,才能对其进行键值对赋值:

1.3 指针使用技巧

  • 使用指针是必须要注意

使用指针时,需要小心避免常见错误,例如取消引用空指针或写入无效的内存位置。 为避免这些错误,您应该始终将指针初始化为有效的内存地址,并在取消引用它们之前检查空指针。

  • 将指针与数组、切片和 map 一起使用时要小心

当将指针与切片一起使用时,您需要确保指针的寿命不会超过切片,否则您可能会得到一个指向已被释放的内存的指针。

在 map 中使用指针时,需要注意不要修改 map 的键,因为这会导致意外行为。

在数组中使用指针时也应该小心,因为很容易不小心修改错误的数组元素。

2. go内置数据类型详解

2.1. 数组

2.1.1 数组基础

  1. 数组:是同一种数据类型的固定长度的序列。
  2. 数组定义:var a [len]int,比如:var a [5]int,数组长度必须是常量,且是类型的组成部分。一旦定义,长度不能变。
  3. 长度是数组类型的一部分,因此,var a[5] int和var a[10]int是不同的类型。
  4. 数组可以通过下标进行访问,下标是从0开始,最后一个元素下标是:len-1。
  5. 访问越界,如果下标在数组合法范围之外,则触发访问越界,会panic。
  6. 数组是值类型,赋值和传参会复制整个数组,而不是指针。因此改变副本的值,不会改变本身的值。 7.支持 "=="、"!=" 操作符,因为内存总是被初始化过的。 8.指针数组 [n]*T,数组指针 *[n]T。

2.1.2 数组初始化

一维数组
全局:
    var arr0 [5]int = [5]int{1, 2, 3} //数组的前三个值为1,2,3,未初始化元素值为 0。
    var arr1 = [5]int{1, 2, 3, 4, 5} //数组的5个值分别为1,2,3,4,5
    var arr2 = [...]int{1, 2, 3, 4, 5, 6} //...根据后面的值来确定数组大小
    var str = [5]string{3: "hello world", 4: "tom"} //更具数组的索引来初始化
    局部:
    a := [3]int{1, 2}          
    b := [...]int{1, 2, 3, 4}   
    c := [5]int{2: 100, 4: 200} 
    d := [...]struct {
        name string
        age  uint8
    }{
        {"user1", 10}, // 可省略元素类型。
        {"user2", 20}, // 别忘了最后一行的逗号。
    }

多维数组
全局:
    var arr0 [5][3]int
    var arr1 [2][3]int = [...][3]int{{1, 2, 3}, {7, 8, 9}}
 局部:
    a := [2][3]int{{1, 2, 3}, {4, 5, 6}}
    b := [...][2]int{{1, 1}, {2, 2}, {3, 3}} // 第 2 纬度不能用 "..."。


多维数组遍历:
package main

import (
    "fmt"
)

func main() {

    var f [2][3]int = [...][3]int{{1, 2, 3}, {7, 8, 9}}

    for k1, v1 := range f {
        for k2, v2 := range v1 {
            fmt.Printf("(%d,%d)=%d ", k1, k2, v2)
        }
        fmt.Println()
    }
}

2.2 切片

2.2.1 切片的内部实现和基础功能

切片是围绕着动态数组的概念来构建的,它跟数组类似,是用于管理数据集合的一种数据结构。

数组一旦创建就不能更改其长度和类型,而切片就不同,切片可以按需自动增长和缩小,增长一般使用内置的 append 函数来实现,而缩小则是通过对切片再次进行切割来实现。

切片对底层数组进行了抽象,并提供相关的操作方法,其内部包含3个字段:指向底层数组的指针、切片访问的元素的个数(即长度)、切片运行增长到的元素个数(即容量)。

2.2.2 切片的创建与初始化

切片的创建有两种方式:一种是使用 make 函数来创建,另一种是使用字面量方式创建。

// 使用 make 函数创建切片
// var 切片变量 = make([]类型, 长度, 容量)
var slice = make([]int, 5, 5)
make 函数声明切片时,第二个参数必填,但第三个参数可以不填,不写第三个参数时,其容量默认等于长度值,但是如果指定容量,那容量一定不能小于长度。

声明并初始化切片时,可以指定所有的元素,也可以只初始化部分元素,此时需要指定要初始化的元素索引。

// 使用字面量声明并初始化切片
// 在声明切片时,指定切片所有元素
var slice = []int{1, 2, 3}

// 初始化部分元素
// 初始化索引为1的元素为1,索引2为6, 索引5为10
var slice = []int{1:1, 2:6, 5:10}

使用字面量声明切片时,有两种特殊情况:一是空切片,二是 nil 切片。 这两种情况创建出来的切片,其长度为0,是不能直接通过下标的方式来赋值的。


// 情况一:空切片
// 切片的元素值是切片类型的零值,即 int 0, string '', 引用类型 nil
var slice = []int{}
// 尝试赋值会报错:runtime error: index out of range [0] with length 0
slice[0] = 1

// 情况二:nil 切片
var slice []int
// 尝试赋值会报错:runtime error: index out of range [0] with length 0
slice[0] = 1

2.2.2 对切片进行切片

语法 slice[start:end:cap]

start:end 表示切片 slice 进行从索引start 到 end-1的切割(不包含索引end的值)

cap: 指定新切片的容量,默认为slice的长度

// 原切片 slice
var slice = []int{1, 2, 3, 4, 5}

// 对slice进行切片生成新切片 newSlice
// 复制整个 slice 切片
var newSlice = slice[:]

// 复制 索引 1 到 3 的元素, 注意新切片不包含索引为 3 的元素 
var newSlice = slice[1:3] // [2 3]
// 复制 slice 第一个元素 到索引为 2 的元素
// 不用写第一个索引
var newSlice = slice[:2] // [1]

// 复制 slice 索引为2 的元素 到 最后一个元素
// 不用写第二个索引
var newSlice = slice[2:]

2.2.3 对切片操作

  • 切片追加元素,可以使用内置的append函数向切片中追加元素,如果切片的容量不够,则会自动扩容
// 创建一个空的切片
var slice []int

// 向切片中追加元素
slice = append(slice, 1, 2, 3)

// 输出切片中的元素
fmt.Println(slice) // 输出:[1 2 3]

// 向切片中追加元素
slice = append(slice, 4)
fmt.Println(slice) // 输出:[1 2 3 4]
  • 切片的遍历,可以使用for循环或者和for - range关键字来遍历切片中的元素
// 创建一个空的切片
var slice []int

// 向切片中追加元素
slice = append(slice, 1, 2, 3)

// 输出切片中的元素
fmt.Println(slice) // 输出:[1 2 3]

// 向切片中追加元素
slice = append(slice, 4)
fmt.Println(slice) // 输出:[1 2 3 4]
  • 切片的复制,可以使用内置的copy函数将一个切片中的元素复制到另一个切片中
// 创建一个空的切片
var slice []int

// 向切片中追加元素
slice = append(slice, 1, 2, 3)

// 输出切片中的元素
fmt.Println(slice) // 输出:[1 2 3]

// 向切片中追加元素
slice = append(slice, 4)
fmt.Println(slice) // 输出:[1 2 3 4]
  • 切片排序可以使用sort包中的函数对切片进行排序
/ 创建一个包含5个元素的整数切片
slice := []int{5, 2, 6, 3, 1}

// 对切片进行排序
sort.Ints(slice)

// 输出排序后的切片
fmt.Println(slice) // 输出:[1 2 3 5 6]

2.3 map

2.3.1 map基础

map是一种无序的基于key-value的数据结构,Go语言中的map是引用类型,必须初始化才能使用。

可以使用 make 函数来创建一个 Map。make 函数需要传递两个参数,第一个参数是 Map 的类型,第二个参数是 Map 的初始大小。Map 的类型可以使用 Map 关键字来定义,make(map[keytype]valuetype,cap)

// 创建一个类型为map[string]int的map,初始大小为10
m := make(map[string]int, 10)

也可以通过字面量来创建

func main() {
    userInfo := map[string]string{
        "username": "pprof.cn",
        "password": "123456",
    }
    fmt.Println(userInfo) //
}

可以使用赋值操作符来向 Map 中添加元素,可以通过map[key]获取value,Go语言中有个判断map中键是否存在的特殊写法,格式如下:

    value, ok := map[key]

func main() {
    scoreMap := make(map[string]int)
    scoreMap["张三"] = 90
    scoreMap["小明"] = 100
    // 如果key存在ok为true,v为对应的值;不存在ok为false,v为值类型的零值
    v, ok := scoreMap["张三"]
    if ok {
        fmt.Println(v)
    } else {
        fmt.Println("查无此人")
    }
}

可以使用 for 循环来遍历 Map 中的所有元素。在每次循环中,将会返回当前元素的键和值。可以通过_忽略不需要的值,例如:

// 遍历map中的元素
for key, value := range m {
    fmt.Println(key, value)
}

2.3.2 map的高级用法

  • 使用delete()函数删除键值对,delete() 函数接受两个参数:要删除元素的 Map 和要删除元素的键
func main(){
    scoreMap := make(map[string]int)
    scoreMap["张三"] = 90
    scoreMap["小明"] = 100
    scoreMap["王五"] = 60
    delete(scoreMap, "小明")//将小明:100从map中删除
    for k,v := range scoreMap{
        fmt.Println(k, v)
    }
}
  • map的值为函数

在 Map 中,值可以是函数。这种用法非常实用,可以让我们更加灵活地编写代码。例如,我们可以使用 Map 来存储不同的操作,然后根据需要调用这些操作:

var operations = map[string]func(int, int) int {
    "add": func(a, b int) int { return a + b },
    "sub": func(a, b int) int { return a - b },
    "mul": func(a, b int) int { return a * b },
}

result := operations["add"](3, 4)

这里,我们创建了一个 Map,它的键是字符串,值是函数类型。然后,我们可以使用这些函数来执行不同的操作,如计算加法、减法、乘法等。

  • key为map类型的切片

下面的代码演示了切片中的元素为map类型时的操作:

func main() {
    var mapSlice = make([]map[string]string, 3)
    for index, value := range mapSlice {
        fmt.Printf("index:%d value:%v\n", index, value)
    }
    fmt.Println("after init")
    // 对切片中的map元素进行初始化
    mapSlice[0] = make(map[string]string, 10)
    mapSlice[0]["name"] = "王五"
    mapSlice[0]["password"] = "123456"
    mapSlice[0]["address"] = "红旗大街"
    for index, value := range mapSlice {
        fmt.Printf("index:%d value:%v\n", index, value)
    }
}
  • value值为切片类型的map 下面的代码演示了map中值为切片类型的操作:
func main() {
    var sliceMap = make(map[string][]string, 3)
    fmt.Println(sliceMap)
    fmt.Println("after init")
    key := "中国"
    value, ok := sliceMap[key]
    if !ok {
        value = make([]string, 0, 2)
    }
    value = append(value, "北京", "上海")
    sliceMap[key] = value
    fmt.Println(sliceMap)
}