Golang-3-struct、slice、映射

Parts of A Tour Of Go

指针

Go具有指针,指针保存了变量的内存地址。
类型*T是指向T类型值的指针,其零值为nil
&操作符会生成一个指向其操作数的指针。
*操作符表示指针指向的底层值。

1
2
3
4
5
6
7
8
9
// 类型声明
var p *int
// 赋值
i := 55
p = &i
// 通过指针p读取i
fmt.Println(*p)
// 通过指针p修改i
*p = 66

C不同,Go没有指针运算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import "fmt"

func main() {
i, j := 92, 2701

// 指针p指向i
p := &i
// 通过指针p读取i
fmt.Println(*p)
// 通过指针p修改i
*p = 21
fmt.Println(i)

p = &j
*p = *p / 37
fmt.Println(j)
}

结构体

一个结构体struct就是一个字段的集合。
type声明用于定义类型。

1
2
3
4
5
6
7
8
9
10
11
12
package main

import "fmt"

type Vertex struct {
X int
Y int
}

func main() {
fmt.Println(Vertex{1, 2})
}

结构体字段用点号来访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "fmt"

type Vertex struct {
X int
Y int
}

func main() {
v := Vertex{1, 2}
v.X = 5
fmt.Println(v)
}

结构体字段也可以用结构体指针来访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import "fmt"

type Vertex struct {
X int
Y int
}

func main() {
v := Vertex{1, 2}
// 声明一个指向结构体v的指针p
p := &v
// 间接引用
(*p).X = 18
// 隐式间接引用
p.Y = 2e9
fmt.Println(v)
}

可以通过直接列出字段的值来分配一个结构体。
使用Name:语法可以仅列出部分字段(顺序无关)。
特殊前缀&返回一个指向结构体的指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import "fmt"

type Vertex struct {
X, Y int
}

var (
// X: 1 Y: 2
v1 = Vertex{1, 2}
// X: 1 Y: 0
v2 = Vertex{X: 1}
// X: 0 Y: 0
v3 = Vertex{}
// type *Vertex
p = &Vertex{1, 2}
)

func main() {
fmt.Println(v1, v2, v3, p)
}

数组

类型[n]T表示拥有nT类型的值的数组。
数组的长度是其类型的一部分,因此数组不能改变大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import "fmt"

func main() {
// 声明
var a [2]string
// 赋值
a[0] = "Hello"
a[1] = "World"
fmt.Println(a[0], a[1])
fmt.Println(a)

// 声明时赋值
primes := [6]int{2, 3, 5, 7, 11, 13}
fmt.Println(primes)
}

切片

每个数组的大小都是固定的,而切片可以实现动态大小。
在实践中,切片比数组更常用。
类型[]T表示一个元素类型为T的切片。

1
a[0:5] // 为数组a的前五个元素创建一个切片
1
2
3
4
5
6
7
8
9
10
11
12
package main

import "fmt"

func main() {
// 声明数组(底层数组)
primes := [6]int{2, 3, 5, 7, 11, 13}
// 声明切片
var s []int = primes[1:4]

fmt.Println(s)
}

切片就像数组的引用

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "fmt"

func main() {
names := [4]string{
"John",
"Paul",
"George",
"Ringo",
}
fmt.Println(names) // [John Paul George Ringo]

a := names[0:2]
b := names[1:3]
fmt.Println(a, b) // [John Paul] [Paul George]

b[0] = "XXX"
fmt.Println(a, b) // [John XXX] [XXX George]
fmt.Println(names) // [John XXX George Ringo]
}

切片语法

切片语法类似于没有长度的数组语法。

1
2
3
4
5
// 数组语法
[3]bool{true, false, true}
// 切片语法
// 会创建一个和上面一样的数组,并构建一个引用了它的切片
[]bool{true, false, true}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import "fmt"

func main() {
q := []int{2, 3, 5, 7, 11, 13}
fmt.Println(q)

r := []bool{true, false, true, true, false, true}
fmt.Println(r)

s := []struct {
i int
b bool
}{
{2, true},
{3, false},
{5, true},
{7, true},
{11, false},
{13, true},
}
fmt.Println(s)
}

切片的默认行为

在进行切片时,你可以利用它的默认行为来忽略上下界。
切片下界默认值为0,上界的默认值为该切片的长度。

1
2
3
4
5
6
var a [10]int
// 对于数组a来说,以下切片是等价的
s1 := a[0:10]
s2 := a[:10]
s3 := a[0:]
s4 := a[:]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import "fmt"

func main() {
s := []int{2, 3, 5, 7, 11, 13}

s = s[1:4]
fmt.Println(s) // [3 5 7]

s = s[:2]
fmt.Println(s) // [3 5]

s = s[1:]
fmt.Println(s) // [5]
}

切片的长度和容量

切片拥有长度容量两个属性。
切片的长度就是它所包含的元素个数。
切片的容量是从它第一个元素开始数,到其底层数组元素末尾的个数。
切片s的长度和容量可以通过表达式len(s)cap(s)来获取。
可以通过重新切片来扩展一个切片,给它提供足够的容量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import "fmt"

func main() {
s := []int{2, 3, 5, 7, 11, 13}
printSlice(s) // len=6, cap=6 [2 3 5 7 11 13]

// 0长度切片 - len变小 cap不变
s = s[:0]
printSlice(s) // len=0, cap=6 []

// 扩展长度 - len变大 cap不变
s = s[:4]
printSlice(s) // len=4, cap=6 [2 3 5 7]

// 丢弃前两个数 - len变小 cap变小
s = s[2:]
printSlice(s) // len=2, cap=4 [5 7]

printSlice(s[:4]) // len=4, cap=4 [5 7 11 13]
printSlice(s[:5]) // painc: runtime error: slice bounds out of range
}

func printSlice(s []int) {
fmt.Printf("len=%d, cap=%d %v \n", len(s), cap(s), s)
}

len只能在cap范围内改变。

nil切片

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

1
2
3
4
5
6
7
8
9
10
11
package main

import "fmt"

func main() {
var s []int
fmt.Println(s, len(s), cap(s)) // [] 0 0
if s == nil {
fmt.Println("slice nil")
}
}

make创建切片

切片可以使用内建函数make来创建。
make函数会创建一个元素全为零值的数组,并返回一个引用了该数组的切片。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "fmt"

func main() {
a := make([]int, 5)
printSlice("a", a) // a len=5 cap=5 [0 0 0 0 0]

b := make([]int, 0, 5)
printSlice("b", b) // b len=0 cap=5 []

c := b[:2]
printSlice("c", c) // c len=2 cap=5 [0 0]

d := c[2:5]
printSlice("d", d) // d len=3 cap=3 [0 0 0]
}

func printSlice(s string, x []int) {
fmt.Printf("%s len=%d cap=%d %v \n", s, len(x), cap(x), x)
}

切片的切片

切片可包含任何类型,包括其它切片。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import (
"fmt"
"strings"
)

func main() {
// 九宫格
board := [][]string{
[]string{"_", "_", "_"},
[]string{"_", "_", "_"},
[]string{"_", "_", "_"},
}

// 依次填入 "X" or "O"
board[0][0] = "X"
board[2][2] = "O"
board[1][2] = "X"
board[1][0] = "O"
board[0][2] = "X"

// 打印九宫格
for i := 0; i < len(board); i++ {
// 在字符中插入空格
fmt.Printf("%s\n", strings.Join(board[i], " "))
}
}

向切片追加元素append

Go提供了内建函数append为切片追加新的元素。

1
func append(s []T, vs ...T) []T

append的第一个参数s是一个元素类型为T的切片,其余类型为T的值将会追加到该切片的末尾。
s的底层数组太小,不足以容纳所有给定的值时,append会分配一个更大的数组,返回的切片会指向这个新分配的数组。
Go切片:用法和本质

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "fmt"

func main() {
var s []int
printSlice(s) // len=0 cap=0 []

s = append(s, 0)
printSlice(s) // len=1 cap=1 [0]

s = append(s, 1)
printSlice(s) // len=2 cap=2 [0 1]

s = append(s, 2, 3, 4, 5, 6)
printSlice(s) // len=7 cap=8 [0 1 2 3 4 5 6]
}

func printSlice(x []int) {
fmt.Printf("len=%d cap=%d %v\n", len(x), cap(x), x)
}

range

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

1
2
3
4
5
6
7
8
9
10
11
package main

import "fmt"

var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}

func main() {
for i, v := range pow {
fmt.Printf("2**%d = %d\n", i, v)
}
}

可以将下标或值赋予_来忽略它。
若只需要索引,去掉, value的部分即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import "fmt"

func main() {
pow := make([]int, 10)

for i := range pow {
pow[i] = 1 << uint(i)
}

for _, value := range pow {
fmt.Printf("%d\n", value)
}
}

练习: 切片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main

import "golang.org/x/tour/pic"

func Pic(dx, dy int) [][]uint8 {
var reply [][]uint8
var temp []uint8
for x := 0; x < dx; x++ {
temp = temp[:0]
for y := 0; y < dy; y++ {
temp = append(temp, uint8(x*y))
}
reply = append(reply, temp)
}
return reply
}

func Pic2(dx, dy int) [][]uint8 {
ret := make([][]uint8, dy)
for i := 0; i < dy; i++ {
ret[i] = make([]uint8, dx)
for j := 0; j < dx; j++ {
ret[i][j] = uint8(i * j)
}
}
return ret
}

func main() {
pic.Show(Pic)
pic.Show(Pic2)
}

映射

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

type Vertex struct {
Lat, Long float64
}

// nil map
var m map[string]Vertex

func main() {
m = make(map[string]Vertex)
m["Bell Labs"] = Vertex{
90.68133, -38.183928,
}
fmt.Println(m["Bell Labs"])
}

映射语法

映射的语法与结构体相似,不过必须有键名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

type Vertex struct {
Lat, Long float64
}

var m = map[string]Vertex{
"Bell Labs": Vertex{
90.68133, -38.183928,
},
"Google": Vertex{
901.682133, -366.183213,
},
}

func main() {
fmt.Println(m)
}

若顶级类型只是一个类型名,可以在语法中省略它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

type Vertex struct {
Lat, Long float64
}

var m = map[string]Vertex{
"Bell Labs": {
90.68133, -38.183928,
},
"Google": {
901.682133, -366.183213,
},
}

func main() {
fmt.Println(m)
}

修改映射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import "fmt"

func main() {
// 声明
m := make(map[string]int)

// 在映射中插入元素
m["Answer"] = 1
// 获取元素
fmt.Println("The Value: ", m["Answer"])

// 在映射中修改元素
m["Answer"] = 2
fmt.Println("The Value: ", m["Answer"])

// 删除元素
delete(m, "Answer")
fmt.Println("The Value: ", m["Answer"])

// 通过双赋值检测某个键是否存在
// var ok bool
// 若 key 在映射中,ok 为 true;否则 ok 为 false
// 若 key 不在映射中,那么 value 是该映射元素类型的零值
// 当从映射中读取某个不存在的键时,结果是映射元素类型的零值
v, ok := m["Answer"]
fmt.Println("The Value: ", v, "Present? ", ok)
}

练习: 映射

实现WordCount。它应当返回一个映射,其中包含字符串中每个单词的个数。
函数wc.Test会对此函数执行一系列测试用例,并输出成功还是失败。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
"golang.org/x/tour/wc"
"strings"
)

func WordCount(s string) map[string]int {
var ret = make(map[string]int)
var temp = strings.Fields(s)
for _, one := range temp {
num, ok := ret[one]
if ok {
ret[one] = num + 1
} else {
ret[one] = 1
}
}
return ret
}

func main() {
wc.Test(WordCount)
}

函数值

函数可以像其它值一样传递。
函数值可以用作函数的参数或返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
"fmt"
"math"
)

// compute函数的参数是一个函数,并返回一个float64
// 作为参数的函数接受两个float64的参数,并返回一个float64
func compute(fn func(float64, float64) float64) float64 {
return fn(3, 5)
}

func main() {
// hypot 是一个函数
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函数可以是一个闭包。闭包是一个函数值,它引用了其函数体之外的变量。
该函数可以访问并赋予其引用的变量的值,换句话说,这些变量被“绑定”在该函数中了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package main

import "fmt"

// 函数 adder 不接受参数,返回一个函数(闭包)
// 作为返回值的函数接受一个int参数,返回一个int
// 每个 adder 函数返回的函数闭包独立拥有一个 sum
func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}

// func pos 和 func neg 各自绑定一个sum变量
// 0 0 0
// 1 1 -2
// 2 3 -6
// 3 6 -12
// 4 10 -20
// 5 15 -30
// 6 21 -42
// 7 28 -56
// 8 36 -72
// 9 45 -90
func main() {
pos, neg := adder(), adder()
for i := 0; i < 10; i++ {
fmt.Println(
i,
pos(i),
neg(-2*i),
)
}
}

练习: 斐波纳契闭包

实现一个fibonacci函数,它返回一个函数(闭包),该闭包返回一个斐波纳契数列 (0, 1, 1, 2, 3, 5, ...)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import "fmt"

// fibonacci is a function that returns
// a function that returns an int.
func fibonacci() func() int {
tempL0 := 0
tempL1 := 1

retFunc := func() int {
tempL0, tempL1 = tempL1, tempL0
tempL1 = tempL0 + tempL1
return tempL1
}
return retFunc
}

func main() {
f := fibonacci()
for i := 0; i < 10; i++ {
fmt.Println(f())
}
}