Featured image of post Go 萌新的初学之路 II

Go 萌新的初学之路 II

Go学习日记(二):结构体、切片和映射

本文属于 Golang 学习日记 系列:
  1. Go 萌新的初学之路
  2. Go 萌新的初学之路 II (本文)
  3. Go 萌新的初学之路 III
  4. Go 萌新的初学之路 IV
  5. Go 萌新的初学之路 V

指针

*T 是指向 T 类型值的指针,其零值为 nil。如:

  • var p *int

& 操作符会生成一个指向其操作数的指针。如:

  • i := 42
  • p = &i

* 操作符表示指针指向的底层值:

  • fmt.Println(*p) // 通过指针 p 读取
  • i *p = 21 // 通过指针 p 设置 i

这也就是通常所说的「解引用」或「间接引用」。

注意:Go 没有指针运算。

 1package main
 2
 3import "fmt"
 4
 5func main() {
 6    i, j := 42, 2701
 7
 8    p := &i         // 指向 i
 9    fmt.Println(*p) // 通过指针读取 i 的值
10    // output: 42
11    *p = 21         // 通过指针设置 i 的值
12    fmt.Println(i)  // 查看 i 的值
13    // output: 21
14    p = &j         // 指向 j
15    *p = *p / 37   // 通过指针对 j 进行除法运算
16    fmt.Println(j) // 查看 j 的值
17    // output: 73
18}

结构体(struct)就是一组 字段(field)

 1package main
 2
 3import "fmt"
 4
 5type Vertex struct {
 6    X int
 7    Y int
 8}
 9
10func main() {
11    fmt.Println(Vertex{5, 2})  // output: {5, 2}
12}

结构体字段可通过点号 . 来访问。

 1package main
 2
 3import "fmt"
 4
 5type Vertex struct {
 6    X int
 7    Y int
 8}
 9
10func main() {
11    v := Vertex{1, 2}
12    v.X = 4
13    fmt.Println(v.X)  // output: 4
14}

结构体字段可通过结构体指针来访问。如果我们有一个指向结构体的指针 p 那么可以通过 (*p).X 来访问其字段 X。 不过这么写太啰嗦了,所以语言也允许我们使用隐式解引用,直接写 p.X 就可以。

 1package main
 2
 3import "fmt"
 4
 5type Vertex struct {
 6    X int
 7    Y int
 8}
 9
10func main() {
11    v := Vertex{1, 2}
12    p := &v
13    p.X = 1e9
14    fmt.Println(v)  // output: {1000000000 2}
15}

使用 Name: 语法可以仅列出部分字段(字段名的顺序无关);特殊的前缀 & 返回一个指向结构体的指针。以下面的代码为例,v2 = Vertex{X: 1} 里只指定了 X,那么 Y 就会置0。

 1package main
 2
 3import "fmt"
 4
 5type Vertex struct {
 6    X, Y int
 7}
 8
 9var (
10    v1 = Vertex{1, 2}  // 创建一个 Vertex 类型的结构体
11    v2 = Vertex{X: 1}  // Y:0 被隐式地赋予零值
12    v3 = Vertex{}      // X:0 Y:0
13    p  = &Vertex{1, 2} // 创建一个 *Vertex 类型的结构体(指针)
14)
15
16func main() {
17    fmt.Println(v1, p, v2, v3) // output: {1 2} &{1 2} {1 0} {0 0}
18}

类型 [n]T 表示一个数组,它拥有 n 个类型为 T 的值。 var a [10]int 会将变量 a 声明为拥有 10 个整数的数组。数组的长度是其类型的一部分,因此数组不能改变大小。 不过没关系,Go 拥有更加方便的使用数组的方式。

 1package main
 2
 3import "fmt"
 4
 5func main() {
 6    var a [2]string
 7    a[0] = "Hello"
 8    // a[1] = "World"
 9    fmt.Println(a[0], a[1]) // output: Hello   
10    fmt.Println(a) // output: [Hello ]
11
12    primes := [6]int{2, 3, 5, 7, 11, 13}
13    fmt.Println(primes) // output: [2 3 5 7 11 13]
14}

切片

每个数组的大小都是固定的。而切片则为数组元素提供了动态大小的、灵活的视角。 在实践中,切片比数组更常用。类型 []T 表示一个元素类型为 T 的切片。切片通过两个下标来界定,一个下界和一个上界,二者以冒号分隔: a[low : high]
与python一样,它会选出一个半闭半开区间,包括第一个元素,但排除最后一个元素。以下表达式创建了一个切片,它包含 a 中下标从 1 到 3 的元素: a[1:4]

 1package main
 2
 3import "fmt"
 4
 5func main() {
 6    primes := [6]int{2, 3, 5, 7, 11, 13}
 7
 8    var s []int = primes[1:4]
 9    fmt.Println(s) // output: [3 5 7]
10}

切片就像数组的引用,并不存储任何数据,它只是描述了底层数组中的一段。更改切片的元素会修改其底层数组中对应的元素,和它共享底层数组的切片都会观测到这些修改。

 1package main
 2
 3import "fmt"
 4
 5func main() {
 6    names := [4]string{
 7        "John",
 8        "Paul",
 9        "George",
10        "Ringo",
11    }
12    fmt.Println(names) // output: [John Paul George Ringo]
13
14    a := names[0:2]
15    b := names[1:3]
16    fmt.Println(a, b) // output: [John Paul] [Paul George]
17
18    b[0] = "XXX"
19    fmt.Println(a, b) // output: [John XXX] [XXX George]
20    fmt.Println(names) // output: [John XXX George Ringo]
21}

切片字面量:类似于没有长度的数组字面量。切片字面量的工作机制是:

  1. 根据字面量中的元素数量,隐式创建一个对应的数组,数组的长度等于元素数量。
  2. 创建一个切片,该切片引用这个隐式创建的数组,切片的长度和容量都等于数组的长度。
  3. 这个切片变量可以像其他切片一样操作,比如追加元素、重新切片等,可能会触发底层数组的扩容或修改。

切片字面量的底层数组只能通过该切片来访问,没有其他变量引用这个数组,所以当切片本身被传递时,底层数组不会被复制,而是共享。这和其他切片操作的行为是一致的。这样可以让切片的初始化更加方便,不需要显式创建数组再转换,同时保持了底层数组的引用机制。

 1package main
 2
 3import "fmt"
 4
 5func main() {
 6    q := []int{2, 3, 5, 7, 11, 13}
 7    fmt.Println(q) // output: [2 3 5 7 11 13]
 8
 9    r := []bool{true, false, true, true, false, true}
10    fmt.Println(r) // output: [true false true true false true]
11
12    s := []struct {
13        i int
14        b bool
15    }{
16        {2, true},
17        {3, false},
18        {5, true},
19        {7, true},
20        {11, false},
21        {13, true},
22    }
23    fmt.Println(s) // output: [{2 true} {3 false} {5 true} {7 true} {11 false} {13 true}]
24}

在进行切片时,你可以利用它的默认行为来忽略上下界。切片下界的默认值为 0,上界则是该切片的长度。对于数组 var a [10]int 来说,以下切片表达式和它是等价的:
a[0:10]a[:10]a[0:]a[:]

 1package main
 2
 3import "fmt"
 4
 5func main() {
 6    s := []int{2, 3, 5, 7, 11, 13}
 7
 8    s = s[1:4]
 9    fmt.Println(s) // output: [3 5 7]
10
11    s = s[:2]
12    fmt.Println(s) // output: [3 5]
13
14    s = s[1:]
15    fmt.Println(s) // output: [5]
16}

切片拥有 长度容量:切片的长度就是它所包含的元素个数,切片的容量是从它的第一个元素开始数一直到其底层数组元素末尾的个数。切片 s 的长度和容量可通过表达式 len(s)cap(s) 来获取。
通过重新切片来扩展一个切片,给它提供足够的容量。 试着修改示例程序中的切片操作,向外扩展它的长度,看看会发生什么。(当然是会报错啦)

 1package main
 2
 3import "fmt"
 4
 5func main() {
 6    s := []int{2, 3, 5, 7, 11, 13}
 7    printSlice(s) // output: len=6 cap=6 [2 3 5 7 11 13]
 8
 9    // 截取切片使其长度为 0
10    s = s[:0]
11    printSlice(s) // output: len=0 cap=6 []
12
13    // 扩展其长度
14    s = s[:4]
15    printSlice(s) // output: len=4 cap=6 [2 3 5 7]
16
17    // 舍弃前两个值
18    s = s[2:]
19    printSlice(s) // output: len=2 cap=4 [5 7]
20}
21
22func printSlice(s []int) {
23    fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
24}

切片的零值是 nil,nil 切片的长度和容量为 0 且没有底层数组。

 1package main
 2
 3import "fmt"
 4
 5func main() {
 6    var s []int
 7    fmt.Println(s, len(s), cap(s)) // output: [] 0 0
 8    if s == nil {
 9        fmt.Println("nil!") // output: nil!
10    }
11}

切片可以用内置函数 make 来创建,这也是你创建动态数组的方式。make 函数会分配一个元素为零值的数组并返回一个引用了它的切片:

a := make([]int, 5) // len(a)=5

要指定它的容量,需向 make 传入第三个参数:

b := make([]int, 0, 5) // len(b)=0, cap(b)=5
b = b[:cap(b)] // len(b)=5, cap(b)=5
b = b[1:] // len(b)=4, cap(b)=4

 1package main
 2
 3import "fmt"
 4
 5func main() {
 6    a := make([]int, 5)
 7    printSlice("a", a) // output: a len=5 cap=5 [0 0 0 0 0]
 8
 9    b := make([]int, 0, 5)
10    printSlice("b", b) // output: b len=0 cap=5 []
11
12    c := b[:2]
13    printSlice("c", c) // output: c len=2 cap=5 [0 0]
14
15    d := c[2:5]
16    printSlice("d", d) // output: d len=3 cap=3 [0 0 0]
17}
18
19func printSlice(s string, x []int) {
20    fmt.Printf("%s len=%d cap=%d %v\n",
21        s, len(x), cap(x), x)
22}

切片可以包含任何类型,当然也包括其他切片。

点击显示代码
 1package main
 2
 3import (
 4    "fmt"
 5    "strings"
 6)
 7
 8func main() {
 9    // 创建一个井字棋(经典游戏)
10    board := [][]string{
11        []string{"_", "_", "_"},
12        []string{"_", "_", "_"},
13        []string{"_", "_", "_"},
14    }
15
16    // 两个玩家轮流打上 X 和 O
17    board[0][0] = "X"
18    board[2][2] = "O"
19    board[1][2] = "X"
20    board[1][0] = "O"
21    board[0][2] = "X"
22
23    for i := 0; i < len(board); i++ {
24        fmt.Printf("%s\n", strings.Join(board[i], " "))
25    }
26         // output: X _ X
27         // output: O _ X
28         // output: _ _ O
29}

向切片追加元素时可以用内置的 append 函数:
func append(s []T, vs ...T) []T
内置函数的文档对该函数有详细的介绍。

append 的第一个参数 s 是一个元素类型为 T 的切片,其余类型为 T 的值将会追加到该切片的末尾。append 会返回一个包含原切片所有元素加上新添加元素的切片。当 s 的底层数组太小,不足以容纳所有给定的值时,它就会分配一个更大的数组。 返回的切片会指向这个新分配的数组。

(要了解关于切片的更多内容,请阅读文章 Go 切片:用法和本质。)

 1package main
 2
 3import "fmt"
 4
 5func main() {
 6    var s []int
 7    printSlice(s) // output: len=0 cap=0 []
 8
 9    // 可在空切片上追加
10    s = append(s, 0)
11    printSlice(s) // output: len=1 cap=1 [0]
12
13    // 这个切片会按需增长
14    s = append(s, 1)
15    printSlice(s) // output: len=2 cap=2 [0 1]
16
17    // 可以一次性添加多个元素
18    s = append(s, 2, 3, 4)
19    printSlice(s) // output: len=5 cap=6 [0 1 2 3 4]
20}
21
22func printSlice(s []int) {
23    fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
24}

映射

for 循环的 range 形式可遍历切片或映射,每次迭代都会返回两个值。 第一个值为当前元素的下标,第二个值为该下标所对应元素的一份副本。

 1package main
 2
 3import "fmt"
 4
 5var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}
 6
 7func main() {
 8    for i, v := range pow {
 9        fmt.Printf("2**%d = %d\n", i, v)
10    }
11}
点击显示输出结果 20 = 1
2
1 = 2
22 = 4
2
3 = 8
24 = 16
2
5 = 32
26 = 64
2
7 = 128

可以将下标或值赋予 _ 来忽略 range 遍历的变量:
for i, _ := range pow for _, value := range pow

若你只需要索引,忽略第二个变量即可:
for i := range pow

 1package main
 2
 3import "fmt"
 4
 5func main() {
 6    pow := make([]int, 10)
 7    for i := range pow {
 8        pow[i] = 1 << uint(i) // == 2**i
 9    }
10    for _, value := range pow {
11        fmt.Printf("%d\n", value)
12    }
13}
点击显示输出结果 1
2
4
8
16
32
64
128
256
512

map 映射将键映射到值,映射的零值为 nilnil 映射既没有键,也不能添加键。make 函数会返回给定类型的映射,并将其初始化备用。

 1package main
 2
 3import "fmt"
 4
 5type Vertex struct {
 6    Lat, Long float64
 7}
 8
 9var m map[string]Vertex
10
11func main() {
12    m = make(map[string]Vertex)
13    m["Bell Labs"] = Vertex{
14        40.68433, -74.39967,
15    }
16    fmt.Println(m["Bell Labs"]) // output: {40.68433 -74.39967}
17}

映射的字面量和结构体类似,只不过必须有键名。

 1package main
 2
 3import "fmt"
 4
 5type Vertex struct {
 6    Lat, Long float64
 7}
 8
 9var m = map[string]Vertex{
10    "Bell Labs": Vertex{
11        40.68433, -74.39967,
12    },
13    "Google": Vertex{
14        37.42202, -122.08408,
15    },
16}
17
18func main() {
19    fmt.Println(m) // output: map[Bell Labs:{40.68433 -74.39967} Google:{37.42202 -122.08408}]
20}

若顶层类型只是一个类型名,那么你可以在字面量的元素中省略它。

 1package main
 2
 3import "fmt"
 4
 5type Vertex struct {
 6    Lat, Long float64
 7}
 8
 9var m = map[string]Vertex{
10    "Bell Labs": {40.68433, -74.39967},
11    "Google":    {37.42202, -122.08408},
12}
13
14func main() {
15    fmt.Println(m) // output: map[Bell Labs:{40.68433 -74.39967} Google:{37.42202 -122.08408}]
16}

在映射 m 中插入或修改元素:[key] = elem
获取元素:elem = m[key]
删除元素:delete(m, key)
通过双赋值检测某个键是否存在:elem, ok = m[key]
keym 中,oktrue ;否则,okfalse
key 不在映射中,则 elem 是该映射元素类型的零值。
:若 elemok 还未声明,你可以使用短变量声明: elem, ok := m[key]

 1package main
 2
 3import "fmt"
 4
 5func main() {
 6    m := make(map[string]int)
 7
 8    m["答案"] = 42
 9    fmt.Println("值:", m["答案"]) // output: 值: 42
10
11    m["答案"] = 48
12    fmt.Println("值:", m["答案"]) // output: 值: 48
13
14    delete(m, "答案")
15    fmt.Println("值:", m["答案"]) // output: 值: 0
16
17    v, ok := m["答案"]
18    fmt.Println("值:", v, "是否存在?", ok) // output: 值: 0 是否存在? false
19}

函数也是值,可以像其他值一样传递。函数值可以用作函数的参数或返回值。

 1package main
 2
 3import (
 4    "fmt"
 5    "math"
 6)
 7
 8func compute(fn func(float64, float64) float64) float64 {
 9    return fn(3, 4)
10}
11
12func main() {
13    hypot := func(x, y float64) float64 {
14        return math.Sqrt(x*x + y*y)
15    }
16    fmt.Println(hypot(5, 12)) // output: 13
17    fmt.Println(compute(hypot)) // output: 5
18    fmt.Println(compute(math.Pow)) // output: 81
19}

执行流程与输出

  1. 直接调用hypot(5,12):
  • 计算:√(5² + 12²) = √169 = 13 → 输出13
  1. compute(hypot):
  • 传入hypotcompute,执行hypot(3,4)√(9 + 16) = √25 = 5 → 输出5
  1. compute(math.Pow):
  • 传入math.Powcompute,执行3^481 → 输出81

Go 函数可以是一个闭包。闭包是一个函数值,它引用了其函数体之外的变量。 该函数可以访问并赋予其引用的变量值,换句话说,该函数被“绑定”到了这些变量。

例如,函数 adder 返回一个闭包。每个闭包都被绑定在其各自的 sum 变量上。

 1package main
 2
 3import "fmt"
 4
 5func adder() func(int) int {
 6    sum := 0
 7    return func(x int) int {
 8        sum += x
 9        return sum
10    }
11}
12
13func main() {
14    pos, neg := adder(), adder()
15    for i := 0; i < 10; i++ {
16        fmt.Println(
17            pos(i),
18            neg(-2*i),
19        )
20    }
21}
点击显示输出结果 0 0
1 -2
3 -6
6 -12
10 -20
15 -30
21 -42
28 -56
36 -72
45 -90

小结

还是水水的一篇,只为了证明一下自己还在学(进度好像确实有点慢)。唔,在整其他的东西导致有点忙,希望下周能把新手教程全过完,然后参考一下别人的项目。

Licensed under CC BY-NC-SA 4.0
最后更新于 2025-09-16 11:05 +0800
本文属于 Golang 学习日记 系列:
  1. Go 萌新的初学之路
  2. Go 萌新的初学之路 II (本文)
  3. Go 萌新的初学之路 III
  4. Go 萌新的初学之路 IV
  5. Go 萌新的初学之路 V
给博主施舍一个赞吧(;へ:) ❤️