接下来就是最麻烦的结构体和切片了,还有我们最爱的指针(cpp永远的痛)
3.指针
Go和cpp一样,都提供了指针操作的能力。
官方定义:指针保存了值的内存地址。
指针的其实就是一个标志,标志着存储了这个值的内存地址。JS的浅拷贝和深拷贝也是这个问题,但是Go提供了直接对内存地址操作的方式也可以近似的理解为深拷贝。
类型 *T 是指向 T 类型值的指针,其零值为 nil。 (重点,后面会用到很多次)
axapta1·var p *int
& 操作符会生成一个指向其操作数的指针。
makefile1i := 422p = &i
* 操作符表示指针指向的底层值。与 C 不同,Go 没有指针运算。
示例:
Go1package main2 3import "fmt"4 5func main() {6 i, j := 42, 27017 8 p := &i // 指向 i9 fmt.Println(*p) // 通过指针读取 i 的值10 *p = 21 // 通过指针设置 i 的值11 fmt.Println(i) // 查看 i 的值12 13 p = &j // 指向 j14 *p = *p / 37 // 通过指针对 j 进行除法运算15 fmt.Println(j) // 查看 j 的值
4.struct 结构体
一个结构体(struct)就是一组字段 (field)。struct关键字是用来定义一个结构类型。 可以理解为TypeScript中的对象type定义方式。
示例:
Go1 2package main3 4import "fmt"5 6type Vertex struct {7 X int8 Y int9}10 11func main() {12 fmt.Println(Vertex{1, 2})13}
结构体字段可以使用点号 .来访问。(这不就和TS/JS一样嘛)
现在我们定义一个指针指向Vertex。
如果我们有一个指向结构体的指针 p 那么可以通过 (*p).X 来访问其字段 X。 不过这么写太啰嗦了,所以语言也允许我们使用隐式解引用,直接写 p.X 就可以
Go1 2v:=Vertex{1.3}3 4p:=&v5 6p.X=15
5.数组
类型 [n]T 表示一个数组,它拥有 n 个类型为 T 的值。
定义方式:
CSS1var a [10] int
会将变量 a 声明为拥有 10 个整数的数组。
数组的长度是其类型的一部分,因此数组不能改变大小。 这看起来是个限制,不过没关系,Go 拥有更加方便的使用数组的方式.。
示例:
Go1package main2 3import "fmt"4 5func main() {6 var a [2]string7 a[0] = "Hello"8 a[1] = "World"9 fmt.Println(a[0], a[1])10 fmt.Println(a)11 12 primes := [6]int{2, 3, 5, 7, 11, 13}13 fmt.Println(primes)14}
6.切片
每个数组的大小都是固定的。而切片则为数组元素提供了动态大小的、灵活的视角。 在实践中,切片比数组更常用。
切片其实就是切割后的数组。使用类型 []T 表示一个元素类型为 T 的切片。
切片通过两个下标来界定,一个下界和一个上界,二者以冒号分隔:
CSS1a[low : high]
切片是左闭右开区间,以下表达式创建了一个切片,它包含 a 中下标从 1 到 3 的元素:
apache1a[1:4]
示例:
Go1package main2 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)10}
ps:切片就像数组的引用 切片并不存储任何数据,它只是描述了底层数组中的一段。如果你修改了切片中的元素会导致底层数组的改变,所有基于这个数组的切片都会观测到这个修改。
除了从数组切割而来,还有其他办法创建一个切片,就是使用切片字面量。
Go1s := []int{1, 2, 3, 4, 5} // 直接创建长度为5、容量也为5的int切片
在进行切片的时候,你可以利用它的默认行为来忽略上下界。切片下界的默认值为 0,上界则是该切片的长度。
对于数组
CSS1var a [10]int
来说,以下切片表达式和它是等价的:
CSS1a[0:10]2a[:10]3a[0:]4a[:]
切片拥有 长度 和 容量,切片的长度就是它所包含的元素个数。
切片的容量是从它的第一个元素开始数,到其底层数组元素末尾的个数。
切片 s 的长度和容量可通过表达式 len(s) 和 cap(s) 来获取。
你可以通过重新切片来扩展一个切片,给它提供足够的容量。
注意:切片在一定程度上算数组的条件引用,所以他的零值是nil。
nil 切片的长度和容量为 0 且没有底层数组。
如果你需要创建动态数组,可以使用make函数进行创建
make 函数会分配一个元素为零值的数组并返回一个引用了它的切片:
Go1a := make([]int, 5) // len(a)=5
要指定它的容量,需向 make 传入第三个参数:
stylus1b := make([]int, 0, 5) // len(b)=0, cap(b)=52 3b = b[:cap(b)] // len(b)=5, cap(b)=54b = b[1:] // len(b)=4, cap(b)=4
切片可以包含任何类型,当然也可以包含切片。
示例:
Go1package main2 3import (4 "fmt"5 "strings"6)7 8func main() {9 // 创建一个井字棋(经典游戏)10 board := [][]string{11 []string{"_", "_", "_"},12 []string{"_", "_", "_"},13 []string{"_", "_", "_"},14 }15 16 // 两个玩家轮流打上 X 和 O17 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}
如果需要向切片中添加元素,我们需要使用内置的append函数。
append函数参数:第一个参数为需要进行添加的切片,第二个以及后面的参数为添加的元素。
append 的第一个参数 s 是一个元素类型为 T 的切片,其余类型为 T 的值将会追加到该切片的末尾。
append 的结果是一个包含原切片所有元素加上新添加元素的切片。
当 s 的底层数组太小,不足以容纳所有给定的值时,它就会分配一个更大的数组。 返回的切片会指向这个新分配的数组。
示例:
Go1 2package main3 4import "fmt"5 6func main() {7 var s []int8 printSlice(s)9 10 // 可在空切片上追加11 s = append(s, 0)12 printSlice(s)13 14 // 这个切片会按需增长15 s = append(s, 1)16 printSlice(s)17 18 // 可以一次性添加多个元素19 s = append(s, 2, 3, 4)20 printSlice(s)21}22 23func printSlice(s []int) {24 fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)25}
注意:当切片容量不足以容纳新元素时,append()函数会自动扩容。扩容策略如下:
- 首次扩容:新容量为原容量的两倍加上新添加元素的数量。
- 后续扩容:若原容量已达到或超过1000,新容量为原容量的1.25倍加上新添加元素的数量;否则,新容量仍为原容量的两倍加上新添加元素的数量。
扩容可能导致性能开销和数据迁移,因此在预知切片大小的情况下,建议使用make()函数指定合适的初始容量。
常见错误和问题:、
(1)修改切片元素会影响所有引用同一底层数组的切片。理解这一点有助于避免数据竞争和意外修改。
(2)访问切片时,索引超出切片长度会导致panic。确保索引在有效范围内:
(3)append()函数可能改变原切片的地址,因此应始终使用其返回值:
Go1s := []int{1, 2, 3} s = append(s, 4) // 必须使用append的返回值,否则可能丢失新添加的元素
切片和映射的遍历方式:
当使用 for 循环遍历切片时,每次迭代都会返回两个值。 第一个值为当前元素的下标,第二个值为该下标所对应元素的一份副本。
Go1package main2 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}
可以将下标或值赋予 _ 来忽略它。如果只需要索引或者值只需要使用下划线就可以忽略
Go1package main2 3import "fmt"4 5func main() {6 pow := make([]int, 10)7 for i := range pow {8 pow[i] = 1 << uint(i) // == 2**i9 }10 for _, value := range pow {11 fmt.Printf("%d\n", value)12 }13}
7.map映射
map是一种无序的基于key-value的数据结构,Go语言中的map是引用类型,必须初始化才能使用。
类似于JS中的对象,map 映射将键映射到值。映射的零值为 nil 。nil 映射既没有键,也不能添加键。make 函数会返回给定类型的映射,并将其初始化备用。
创建映射的方式推荐使用make函数进行创建: m := make(map[string]int) 其中map是映射关键字,string代表了键的类型,int代表了值的类型。
示例:
Go1 2package main3 4import "fmt"5 6type Vertex struct {7 Lat, Long float648}9 10var m map[string]Vertex11 12func main() {13 m = make(map[string]Vertex)14 m["Bell Labs"] = Vertex{15 40.68433, -74.39967,16 }17 fmt.Println(m["Bell Labs"])18}
映射的字面量和结构体类似,只不过必须有键名。
Go1 2package main3 4import "fmt"5 6type Vertex struct {7 Lat, Long float648}9 10var m = map[string]Vertex{11 "Bell Labs": Vertex{12 40.68433, -74.39967,13 },14 "Google": Vertex{15 37.42202, -122.08408,16 },17}18 19func main() {20 fmt.Println(m)21}
映射的修改,使用m[key] = elem对key的元素进行修改,获取元素对应的值使用elem = m[key],删除元素使用delete(m, key)传入映射以及需要删除的key。
如果我们需要检查某个键是否存在,可以使用elem, ok = m[key]的双赋值方式进行检测。
若 key 在 m 中,ok 为 true ;否则,ok 为 false。
若 key 不在映射中,则 elem 是该映射元素类型的零值。
8.函数闭包
函数也是值。它们可以像其他值一样传递。
函数值可以用作函数的参数或返回值。 Go 函数可以是一个闭包。闭包是一个函数值,它引用了其函数体之外的变量。 该函数可以访问并赋予其引用的变量值,换句话说,该函数被“绑定”到了这些变量。
例如,函数 adder 返回一个闭包。每个闭包都被绑定在其各自的 sum 变量上。