首页 > 技术文章 > A tour of Go (3) - 更多类型:struct, slice, 映射和函数闭包

lsc2019 2020-12-19 21:20 原文

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 表示拥有 nT 类型的值的数组。

表达式:

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

切片的零值是 nilnil 切片的长度和容量为 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结果:

img

(i+j)/2结果:

img

i*log(j)结果:

img

i%(j+1)结果:

img

5. 映射

映射将键映射到值。

映射的零值为 nilnil 映射既没有键,也不能添加键。

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] 

keym 中,oktrue ;否则,okfalse

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())
	}
}

推荐阅读