Thursday, December 17, 2020
A tour of Go (3) - 更多类型:struct, slice, 映射和函数闭包
1. 指针
Go 拥有指针。指针保存了值的内存地址。
类型 *T
是指向 T
类型值的指针。其零值为 nil
。
var p *int // 定义一个指向 int 类型的指针
&
操作符会生成一个指向其操作数的指针。
i := 42
p = &i // 将变量地址赋值给指针变量
*
操作符表示指针指向的底层值。
fmt.Println(*p) // 通过指针 p 读取 i
*p = 21 // 通过指针 p 设置 i
与 C 不同,Go 没有指针运算。
2. 结构体
一个结构体(struct
)就是一组字段(field)。
type Vertex struct {
X int
Y int
}
结构体字段:结构体字段使用点号来访问。
v := Vertex{1, 2}
v.X = 4
结构体指针:结构体字段可以通过结构体指针来访问。
可以通过 (*p).X
来访问其字段 X
。允许我们使用隐式间接引用,直接写 p.X
就可以。
p := &v // 不定义指针指向对象的属性
p.X = 1e9
结构体文法:可以根据名字来赋值属性,也可以只赋值部分,其他的根据类型自动赋值为默认值。
使用特殊前缀 &
返回一个指向结构体的指针。
3. 数组
类型 [n]T
表示拥有 n
个 T
类型的值的数组。
表达式:
var a [10]int // 声明一个拥有10个整数的数组
数组的长度是其类型的一部分,因此数组不能改变大小。
4. 切片
类型 []T
表示一个元素类型为 T
的切片。
a[low : high] // 通过冒号来分隔上界和下界
a[1:4] // 半开区间,排除最后一个元素
切片下界的默认值为 0
,上界则是该切片的长度。
切片就像数组的引用,切片本身并不储存任何数据,更改切片的元素会修改其底层数组中对应的元素。
切片文法类似于没有长度的数组文法。
[3]bool{true, true, false} // 数组文法示例
[]bool{true, true, false} // 创建一个和上面相同的数组,然后构建一个引用了它的切片
切片拥有 长度 和 容量。
切片 s
的长度和容量可通过表达式 len(s)
和 cap(s)
来获取。
指定下界会改变切片的容量,而上界则不会。
s := []int{2, 3, 5, 7, 11, 13} // len(s) = 6, cap = 6
s = s[:0] // len(s) = 0, cap = 6, 使用不同上界来引用,不会改变切片的容量!!
s = s[:4] // len(s) = 4, cap = 6, 增大上界,会拓展其长度,不改变容量!!
s = s[2:] // len(s) = 2, cap = 4, 指定下界,会舍弃前面n个值,改变切片的容量!!!
s = s[:5] // 报错:slice bounds out of range [:5] with capacity 4
切片的零值是 nil
。nil
切片的长度和容量为 0 且没有底层数组。
表达式:var s[]int // 声明一个 nil 切片
切片可以用内建函数 make
来创建,这也是你创建动态数组的方式。
make
函数会分配一个元素为零值的数组并返回一个引用了它的切片:
a := make([]int, 5) // len=5 cap=5 [0 0 0 0 0],省略了一个参数,默认len=cap=5
b := make([]int, 0, 5) // len=0 cap=5 []
切片的切片:切片可包含任何类型,甚至包括其它的切片。
board := [][]string{
[]string{"_", "_", "_"},
[]string{"_", "_", "_"},
[]string{"_", "_", "_"},
}
// 通过两个索引值来改变指定的切片值
board[0][0] = "X"
为切片追加新的元素是种常用的操作,为此 Go 提供了内建的 append
函数。
var s []int // len=0 cap=0 []
s = append(s, 0) // len=1 cap=1 [0], append结果必须赋值给原来的切片
s = append(s, 1, 2, 3) // len=4 cap [0 1 2 3], 可以一次性添加多个元素
for
循环的 range
形式可遍历切片或映射。
当使用 for
循环遍历切片时,每次迭代都会返回两个值。第一个值为当前元素的下标,第二个值为该下标所对应元素的一份副本。
for i, value := range pow
for i := range pow // 只要索引,可以直接忽略第第二个变量
for i, _ := range pow // 用 _ 来忽略value
for _, value := range pow // 用 _ 来忽略i
练习:切片 https://tour.go-zh.org/moretypes/18
题目描述:实现 Pic
。它应当返回一个长度为 dy
的切片,其中每个元素是一个长度为 dx
,元素类型为 uint8
的切片。当你运行此程序时,它会将每个整数解释为灰度值(好吧,其实是蓝度值)并显示它所对应的图像。
图像的选择由你来定。几个有趣的函数包括 (x+y)/2
, x*y
, x^y
, x*log(y)
和 x%(y+1)
。
(提示:需要使用循环来分配 [][]uint8
中的每个 []uint8
;请使用 uint8(intValue)
在类型之间转换;你可能会用到 math
包中的函数。)
package main
import (
"golang.org/x/tour/pic"
"math"
)
func Pic(dx, dy int) [][]uint8 {
img := make([][]uint8, dy) // 初始化切片,这里用 :=
for i:=0; i<dy; i++ {
img[i] = make([]uint8, dx) // 这里用 = ,不用 :=
for j:=0; j<dx; j++ {
img[i][j] = uint8(float64(i)*math.Log(float64(j))) //必须得类型转换,再赋值
}
}
return img
}
func main() {
pic.Show(Pic)
}
i*j
结果:
(i+j)/2
结果:
i*log(j)
结果:
i%(j+1)
结果:
5. 映射
映射将键映射到值。
映射的零值为 nil
。nil
映射既没有键,也不能添加键。
make
函数会返回给定类型的映射,并将其初始化备用。
type Vertex struct {
Lat, Long float64
}
func main(){
m := make(map[string]Vertex) //初始化
m["Bell Labs"] = Vertex{
40.68433, -74.39967,
}
fmt.Println(m["Bell Labs"]) // {40.68433 -74.39967}
}
映射的文法:映射的文法与结构体相似,不过必须有键名。
var m = map[string]Vertex{ // 赋值必须有键名
"Bell Labs": Vertex{
40.68433, -74.39967,
},
"Google": Vertex{
37.42202, -122.08408,
},
}
fmt.Println(m)
// map[Bell Labs:{40.68433 -74.39967} Google:{37.42202 -122.08408}]
若顶级类型只是一个类型名,你可以在文法的元素种省略它。(?)
var m = map[string]Vertex{
"Bell Labs": {40.68433, -74.39967}, // 省略了Vertex
"Google": {37.42202, -122.08408},// 省略了Vertex
}
修改映射:
在映射 m
中插入或修改元素:
m[key] = elem // m["Answer"] = 42
获取元素:
elem = m[key] // val = m["Answer"]
删除元素:
delete(m, key) // delete(m, "Answer")
// m["Answer"] = 0 从映射中读取某个不存在的键时,结果是映射的元素类型的零值。
通过双赋值检测某个键是否存在:
// 若 elem 或 ok 还未声明,使用短变量声明
elem, ok := m[key]
// 声明过,则直接 =
elem, ok = m[key]
若 key
在 m
中,ok
为 true
;否则,ok
为 false
。
若 key
不在映射中,那么 elem
是该映射元素类型的零值。
练习:映射 https://tour.go-zh.org/moretypes/23
实现 WordCount
。它应当返回一个映射,其中包含字符串 s
中每个“单词”的个数。函数 wc.Test
会对此函数执行一系列测试用例,并输出成功还是失败。
你会发现 strings.Fields 很有帮助。
package main
import (
"golang.org/x/tour/wc"
"strings" // strings.Fields会去掉string里的空格,返回一个个词
)
func WordCount(s string) map[string]int {
count := make(map[string]int) // 根据函数返回值,声明一个映射
for _, v := range strings.Fields(s) { //for-range,第一个位index不用,第二位word
count[v]++ // word作为key值,从零开始,每遇到一次word v,增加1的count
}
return count // 原本这写了个example,改成返回自己定义的映射
}
func main() {
wc.Test(WordCount)
}
6. 函数值
函数也是值。它们可以像其它值一样传递。
函数值可以用作函数的参数或返回值。
func compute(fn func(float64, float64) float64) float64 { // 函数做参数,
return fn(3, 4) // 函数做返回值 // 名字在前,类型在后
}
func main() {
hypot := func(x, y float64) float64 {
return math.Sqrt(x*x + y*y)
}
fmt.Println(hypot(5, 12))
fmt.Println(compute(hypot)) // 函数值传递
fmt.Println(compute(math.Pow))
}
函数的闭包:
Go 函数可以是一个闭包。闭包是一个函数值,它引用了其函数体之外的变量。该函数可以访问并赋予其引用的变量的值,换句话说,该函数被这些变量“绑定”在一起。
例如,函数 adder
返回一个闭包。每个闭包都被绑定在其各自的 sum
变量上。
package main
import "fmt"
func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}
func main() {
pos, neg := adder(), adder()
for i := 0; i < 10; i++ {
fmt.Println(
pos(i),
neg(-2*i),
)
}
}
// 结果: 0 0;1 -2;3 -6;6 -12;···
再比如:
package main
import (
"fmt"
)
// 定义一个square函数,返回值类型是func() int,返回的这个函数就是一个闭包。
// 闭包是函数和它所引用的环境,也就是闭包=函数+引用环境。
func squares() func() int {
var x int
return func() int {
x++
return x * x
}
}
func main() {
f1 := squares()
f2 := squares()
fmt.Println("first call f1:", f1()) // 1
fmt.Println("second call f1:", f1()) // 4
fmt.Println("first call f2:", f2()) // 1
fmt.Println("second call f2:", f2()) // 4
}
练习:斐波那契数列闭包 https://tour.go-zh.org/moretypes/26
实现一个 fibonacci
函数,它返回一个函数(闭包),该闭包返回一个斐波纳契数列 (0, 1, 1, 2, 3, 5, ...)
。
package main
import "fmt"
// 返回一个“返回int的函数”
func fibonacci() func() int {
one := 0 // 数列第一个数=0,列出来
two := 1 // 数列第二个数=1,列出来
return func() int{
result := one // 每次重新定义result,结果为数列第i位的值!!
one = two // 数列第i+1位的值!(比result多一位)
two += result // 数列第i+2位的值!(比result多两位)
return result
}
}
func main() {
f := fibonacci()
for i := 0; i < 10; i++ {
fmt.Println(f())
}
}