Re:从零开始的 Go 之旅- 基础语法(3)

本文最后更新于 2025年3月9日 晚上

继续 Go

条件语句

if,if…else,if 嵌套,switch,select,goto

select 类似于 switch,select 会随机执行可运行的 case,没有 case 可运行的话,会阻塞到有 case 可运行

if

1
2
3
4
5
6
7
8
func main() {
a, b := 1, 2
if a < b {
fmt.Println("hhh")
} else {
fmt.Println("xxx")
}
}

switch

1
2
3
4
5
6
7
8
switch expr {
case case1:
statement1
case case2:
statement2
default:
default statement
}

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {
str := "a"
switch str {
case "a":
str += "a"
str += "c"
case "b":
str += "bb"
str += "aa"
default:
str += "cc"
}
fmt.Println(str)
}
>>aac

还可以在表达式之前声明变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
num := 2
switch {
case num >= 0 && num <= 1:
num++
case num > 1:
num--
fallthrough
case num < 0:
num += num
}
fmt.Println(num)
}
>> 2

fallthrough 关键字会继续执行相邻的下一个分支,不然会自动跳出,在 go 中的 switch 相当于是 c 语言中每个 case 后面添加了 break 语句,所以会自动终止。``fallthroug它会无条件地执行下一个 case 语句,而不会进行条件检查,所以例子中的num经过了-,又经过了 +` , 并没有变化。

循环语句

for

go 中只有一个 for 循环,一般格式

1
2
3
for init statement; expression; post statement {
execute statement
}

当成 while 来使用时

1
2
3
for expression {
execute statement
}

打印数组

1
2
3
4
5
6
7
8
9
func main() {
var b int = 6
//var a int
numbers := [6]int{1, 2, 3, 5, 7, 9}
for a := 0; a < b; a++ {
fmt.Print(numbers[a])
}
}
>> 123579

双循环

1
2
3
4
5
6
7
8
9
10
func main() {
for i := 1; i <= 9; i++ {
for j := 1; j <= 9; j++ {
if i <= j {
fmt.Printf("%d * %d = %2d ", i, j, i*j)
}
}
fmt.Println()
}
}

for range

for 循环的 range 格式可以对 slice、map、数组、字符串、切片、映射表,channel 等进行迭代循环,一般格式

1
2
3
for index, value := range iterable {
// body
}

数组 Array

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
//全局
var arr0 [5]int = [5]int{1, 2, 3}
var arr1 = [5]int{1, 2, 3, 4, 5}
var arr2 = [...]int{1, 2, 3, 4, 5, 6}
var str = [5]string{3: "hhhhh", 4: "xxxxxx"}

func main() {
//局部
a := [3]int{1, 2}
b := [...]int{1, 2, 3, 4}
c := [5]int{2: 100, 4: 200}
d := [...]struct {
name string
age uint8
}{
{"a", 10},
{"b", 20},
}
fmt.Println(arr0, arr1, arr2, str)
fmt.Println(a, b, c, d)

}
>>
[1 2 3 0 0] [1 2 3 4 5] [1 2 3 4 5 6] [ hhhhh xxxxxx]
[1 2 0] [1 2 3 4] [0 0 100 0 200] [{a 10} {b 20}]

多维数组

1
2
3
4
5
6
7
8
9
10
11
12
13
var arr0 [5][3]int
var arr1 [2][3]int = [...][3]int{{1, 2, 3}, {4, 5, 6}}

func main() {
a := [2][3]int{{1, 2, 3}, {7, 8, 9}}
b := [...][2]int{{1, 1}, {2, 2}, {3, 3}}
fmt.Println(arr0, arr1)
fmt.Println(a, b)

}
>>
[[0 0 0] [0 0 0] [0 0 0] [0 0 0] [0 0 0]] [[1 2 3] [4 5 6]]
[[1 2 3] [7 8 9]] [[1 1] [2 2] [3 3]]

第二维度不能用 ... 来表示,未赋值的数组初始化为 0

多维数组遍历

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
var a [2][3]int = [...][3]int{{1, 2, 3}, {4, 5, 6}}
for k1, v1 := range a {
for k2, v2 := range v1 {
fmt.Printf("(%d,%d)=%d", k1, k2, v2)
}
fmt.Println()
}
}
>>
(0,0)=1 (0,1)=2 (0,2)=3
(1,0)=4 (1,1)=5 (1,2)=6

数组拷贝和传参

值拷贝会造成性能问题,通常使用 slice 或数组指针

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
func printArr(arr *[5]int) {
arr[0] = 10
for k, v := range arr {
fmt.Println(k, v)
}

}
func main() {
var arr1 [5]int
printArr(&arr1)
fmt.Println(arr1)
arr2 := [...]int{2, 4, 6, 8, 10}
printArr(&arr2)
fmt.Println(arr2)
}
>>
0 10
1 0
2 0
3 0
4 0
[10 0 0 0 0]
0 10
1 4
2 6
3 8
4 10
[10 4 6 8 10]

为什么 arr2 数组输出的第一个值不是 2 呢,因为在进入 printArr 函数时,arr[0]=10,所以会被赋值为 10,输出的就是 10

切片 Slice

Slice 既不是数组也不是数组指针,是数组的一个引用,是引用类型。自身是结构体,值拷贝传递。长度可变,一般格式

1
2
var 变量名 []类型
var str []string

创建切片

在 fun main 中

声明切片

1
2
3
4
5
6
7
8
var s1 []int
if s1 == nil {
fmt.Println("null")

} else {
fmt.Println("不是 null")
}

:= 创建

1
2
s2 := []int{}
fmt.Println(s2)

make() 创建

1
2
var s3 []int = make([]int, 0)
fmt.Println(s3)

初始化赋值

1
2
var s4 []int = make([]int, 0, 0)
fmt.Println(s4)

从数组切片

1
2
3
4
5
6
7
8
9
s5 := []int{1, 2, 3}
fmt.Println(s5)
arr := [5]int{1, 2, 3, 4, 5}
var s6 []int
s6 = arr[1:4]
fmt.Println(s6)
>>
[1 2 3]
[2 3 4]

切片初始化

len() 长度,cap() 容量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var arr = [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
var slice0 []int = arr[2:8]
var slice1 []int = arr[0:6]
var slice2 []int = arr[5:10]
var slice3 []int = arr[0:len(arr)]
var slice4 = arr[:len(arr)-1]

func main() {
fmt.Println(arr, slice0, slice1, slice2, slice3, slice4)
arr2 := [...]int{9, 8, 7, 6, 5, 4, 3, 2, 1, 0}
slice5 := arr[2:8]
slice6 := arr[0:6]
slice7 := arr[5:10]
slice8 := arr[0:len(arr)]
slice9 := arr[:len(arr)-1]
fmt.Println(arr2, slice5, slice6, slice7, slice8, slice9)
}
>>
[0 1 2 3 4 5 6 7 8 9] [2 3 4 5 6 7] [0 1 2 3 4 5] [5 6 7 8 9] [0 1 2 3 4 5 6 7 8 9] [0 1 2 3 4 5 6 7 8]
[9 8 7 6 5 4 3 2 1 0] [2 3 4 5 6 7] [0 1 2 3 4 5] [5 6 7 8 9] [0 1 2 3 4 5 6 7 8 9] [0 1 2 3 4 5 6 7 8]

make() 创建切片

1
2
3
var slice []type = make([]type, len)
slice := make([]type, len)
slice := make([]type, len, cap) (类型,长度,容量)
1
2
3
4
5
6
7
var slice1 []int = make([]int, 10)
slice2 := make([]int, 10)
slice3 := make([]int, 10, 10)
>>
[0 0 0 0 0 0 0 0 0 0]
[0 0 0 0 0 0 0 0 0 0]
[0 0 0 0 0 0 0 0 0 0]

创建 slice 数组,自动分配底层数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
s1 := []int{0, 1, 2, 3, 8: 100} //使用索引初始化
fmt.Println(s1, len(s1), cap(s1))

s2 := make([]int, 6, 8)
fmt.Println(s2, len(s2), cap(s2))

s3 := make([]int, 6)
fmt.Println(s3, len(s3), cap(s3))
}
>>
[0 1 2 3 0 0 0 0 100] 9 9
[0 0 0 0 0 0] 6 8
[0 0 0 0 0 0] 6 6

用 make 动态创建 slice ,可以让数组不需要用常量做长度

1
2
3
4
5
6
7
8
func main() {
s := []int{0, 1, 2, 3}
p := &s[2]
*p += 100
fmt.Println(s)
}
>>
[0 1 102 3]

[][]T 表明元素类型为 []T,比如 [][]int 表明元素类型为 []int

1
2
3
4
5
6
7
8
9
10
func main() {
data := [][]int{
[]int{1, 2, 3},
[]int{100, 200},
[]int{11, 22, 33, 44},
}
fmt.Println(data)
}
>>
[[1 2 3] [100 200] [11 22 33 44]]

还可以直接修改 struct array/slice 成员

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
d := [5]struct { //创建一个数组,元素为结构体
x int
}{}
s := d[:]
d[1].x = 10 //修改 d 中的第二个元素为 10
s[2].x = 20 //修改 s 中第三个元素为 20
fmt.Println(d)
fmt.Printf("%p,%p\n", &d, &d[0])
}
>>
[{0} {10} {20} {0} {0}]
0xc000010390,0xc000010390

数组 d 和切片 s 共享同一块底层数据,因此通过数组或切片修改数据会相互影响

append()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {
var a = []int{1, 2, 3}
fmt.Println(a)
var b = []int{4, 5, 6}
fmt.Println(b)
c := append(a, b...)
fmt.Println(c)
d := append(c, 7)
fmt.Println(d)
}
>>
[1 2 3]
[4 5 6]
[1 2 3 4 5 6]
[1 2 3 4 5 6 7]

append 向 slice 尾部添加数据,返回新的对象

超出原 slice.cap 的限制时,会重新分配底层数组,即使是原数组没有填满

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
data := [...]int{0, 1, 2, 3, 4, 10: 0} //自动推导数组长度,数组扩展到长度 11
s := data[:2:3] //从 0 索引到 2 索引,容量为 3

s = append(s, 100, 200)

fmt.Println(s, data)
fmt.Println(&s[0], &data[0])
}
>>
[0 1 100 200] [0 1 2 3 4 0 0 0 0 0 0]
0xc000010390 0xc0000141e0

append 后的 s 重新分配了底层数组

slice 中 cap 重新分配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func main() {
s := make([]int, 0, 1)
c := cap(s)

for i := 0; i < 50; i++ {
s = append(s, i)
if n := cap(s); n > c {
fmt.Printf("%d->%d\n", c, n)
c = n
}

}
}
>>
1->2
2->4
4->8
8->16
16->32
32->64

通常以 2 倍容量来分配底层数组

切片拷贝

copy() 函数复制数据,以长度小的为准,长度小的复制到大的去,指向同一底层数组,允许元素区间重叠

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
func main() {
s1 := []int{1, 2, 3, 4, 5}
fmt.Printf("%v\n", s1)

s2 := make([]int, 10)
fmt.Printf("%v\n", s2)

copy(s2, s1)
fmt.Printf("%v\n", s1)
fmt.Printf("%v\n", s2)

s3 := []int{1, 2, 3}
fmt.Printf("%v\n", s3)

s3 = append(s3, s2...)
fmt.Printf("%v\n", s3)

s3 = append(s3, 4, 5, 6)
fmt.Printf("%v", s3)
}
>>
[1 2 3 4 5]
[0 0 0 0 0 0 0 0 0 0]
[1 2 3 4 5]
[1 2 3 4 5 0 0 0 0 0]
[1 2 3]
[1 2 3 1 2 3 4 5 0 0 0 0 0]
[1 2 3 1 2 3 4 5 0 0 0 0 0 4 5 6]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func main() {
data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
fmt.Println(data)

s1 := data[8:] //取 data[8] 到 data[9]
s2 := data[:5] //取 data[0] 到 data[4]
fmt.Printf("%v\n", s1)
fmt.Printf("%v\n", s2)

copy(s2, s1)
fmt.Printf("%v\n", s1)
fmt.Printf("%v\n", s2)
fmt.Println(data)
}
>>
[0 1 2 3 4 5 6 7 8 9]
[8 9]
[0 1 2 3 4]
[8 9]
[8 9 2 3 4]
[8 9 2 3 4 5 6 7 8 9]

s2 是 data 的引用,所以后面 s2 变化导致了 data 的变化

slice 遍历

跟其他遍历差不多

1
2
3
4
5
6
7
8
9
10
func main() {
data := [...]int{1, 2, 3, 4, 5, 6, 7}
slice := data[:]
for value := range slice {
fmt.Printf("%v,", value)

}
}
>>
0,1,2,3,4,5,6,

切片 resize

调整大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
var a = []int{1, 2, 3, 4, 5}
fmt.Printf("%v,%v\n", a, len(a))

b := a[1:2]
fmt.Printf("%v,%v\n", b, len(b))

c := b[0:3]
fmt.Printf("%v,%v", c, len(c))
}
>>
[1 2 3 4 5],5
[2],1
[2 3 4],3

字符串和切片

string 底层是一个 byte 的数组,所以也能进行切片

1
2
3
4
5
6
7
8
9
10
11
func main() {
str := "butt3rf1y"
s1 := str[0:5]
fmt.Println(s1)

s2 := str[6:]
fmt.Println(s2)
}
>>
butt3
f1y

string 是不可变的,不能直接修改,可以将字符转化为字节切片,修改之后再转为字符

1
2
3
4
5
6
7
8
9
10
11
func main() {
str := "butt3rf1y"
s := []byte(str) //中文字符用 []rune(str)
s[7] = 'h'
s = s[:8]
s = append(s, '!')
str = string(s)
fmt.Println(str)
}
>>
butt3rfh!

多维切片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func main() {
var nums [5][5]int
for _, num := range nums {
fmt.Println(num)
}
fmt.Println()
slices := make([][]int, 5)
for _, slice := range slices {
fmt.Println(slice)
}

}
>>
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]

[]
[]
[]
[]
[]

Go 的数组和切片都是一维的,要创建二维数组或切片需要定义一个数组的数组或切片的切片。从上面例子能看出来,数组初始化时一维和二维长度是固定了,而切片是不固定的,所以切片需要单独初始化

1
2
3
4
5
6
7
8
9
func main() {
slices := make([][]int, 5)
for i := 0; i < len(slices); i++ {
slices[i] = make([]int, 5)
}
fmt.Println(slices)
}
>>
[[0 0 0 0 0] [0 0 0 0 0] [0 0 0 0 0] [0 0 0 0 0] [0 0 0 0 0]]

拓展表达

只有切片才能使用拓展表达,解决切片共享底层数组的读写问题

1
2
3
4
slice[low:high:max]
low: 起始索引
high: 结束索引(不包含)
max: 最大容量

像前面说的,s1 和 s2 共享的是一个底层数组,那么对 s2 进行改变时也会对 s1 有影响,比如说

1
2
3
4
5
6
7
8
9
10
func main() {
s1 := []int{4, 5, 6, 7, 8, 9}
s2 := s1[3:4]
s2 = append(s2, 1)
fmt.Println(s2)
fmt.Println(s1)
}
>>
[7 1]
[4 5 6 7 1 9]

这个就会影响 s1 的数据,向 s2 添加元素,把 s1 更改了,这个时候用拓展表达式就能解决了

1
2
3
4
s2 := s1[3:4:4]
>>
[7 1]
[4 5 6 7 8 9]

clear()

clear() 会将切片内所有的值置为零值

1
2
3
4
5
6
7
func main() {
s := []int{1, 2, 3, 4}
clear(s)
fmt.Println(s)
}
>>
[0 0 0 0]

清空切片

1
2
3
4
5
6
7
func main() {
s := []int{1, 2, 3, 4}
s = s[:0:0]
fmt.Println(s)
}
>>
[]

指针

Go 中是指针不能进行偏移和运算,有两个常用操作符:取地址符 &,解引用符 *

指针地址

& 取地址,取变量指针

1
2
3
ptr := &v	//v 的类型为 T
v:被取地址的变量,类型为 T
ptr:接收地址的变量,ptr 的类型为 *T,为 T 的指针类型

比如说

1
2
3
4
5
6
7
8
9
10
11
func main() {
a := 10
b := &a
fmt.Printf("a:%d ptr:%p\n", a, &a)
fmt.Printf("b:%p type:%T\n", b, b)
fmt.Println(&b)
}
>>
a:10 ptr:0xc00008c098
b:0xc00008c098 type:*int
0xc000090048

指针取值

对普通变量使用 & 取地址后会获得这个变量的指针,然后对指针使用 * 解引用,也就是指针取值

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
a := 10
b := &a
fmt.Printf("%T\n", b)

c := *b
fmt.Printf("%T\n", c)
fmt.Printf("%v\n", c)
}
>>
*int
int
10

取地址操作符 & 和取值操作符 * 是一对互补操作符,& 取出地址,* 根据地址取出地址指向的值

而且 * 还能声明指针

1
2
3
4
func main() {
var numPtr *int
fmt.Println(numPtr)
}

*int 代表变量是 int 型指针,指针必须要初始化,不然就是空指针,可以取地址符将其他变量的地址赋给该指针,还能使用内置函数 new() 手动分配

new()

1
2
3
4
5
6
7
func main() {
var numPtr *int
numPtr = new(int)
fmt.Println(numPtr)
}
>>
0xc00008c098

new() 函数只有一个参数,参数为类型,返回一个对应类型的指针,会对指针分配内存,并且指针指向对应类型的零值

1
2
3
4
5
6
7
8
9
10
11
func main() {
fmt.Println(*new(string))
fmt.Println(*new(int))
fmt.Println(*new([5]int))
fmt.Println(*new([]float64))
}
>>

0
[0 0 0 0 0]
[]

变量、指针地址、指针变量、取地址、取值

1
2
3
对变量进行取地址 (&),可以得到变量的指针变量
指针变量值为指针地址
指针变量进行取值 (*),可以得到指针变量指向的原变量的值

指针传值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func ptr1(x int) {
x = 100
}
func ptr2(x *int) {
*x = 100
}

func main() {
a := 10
ptr1(a)
fmt.Println(a)
ptr2(&a)
fmt.Println(a)
}
>>
10
100

值传递不会影响 a 的值,所以第一次输出 a 的值不变

make

和 new() 一样,是用来分配内存的,但是与 new() 有区别的是,它只用于 slice、map 和 channel 的内存创建,返回的类型就是这三个类型本身,而不是指针类型

1
func make(t Type,size ...IntegerType) Type

比如说

1
2
3
4
5
6
7
8
func main() {
var b map[string]int
b = make(map[string]int, 10)
b["aaa"] = 100
fmt.Println(b)
}
>>
map[aaa:100]

区别

make() 只用于 slice、map 以及 channel 的初始化,返回的还是这三个类型本身

new() 用于类型的内存分配,并对内存对应的值为类型零值,返回指向类型的指针

map

映射表的数据结构实现通常两种:哈希表 (hash table ) 和搜索树 (search tree),无序和有序。在 Go 中 map 的实现是基于哈希桶,所以也无序,必须要初始化才能使用

初始化

1
map[KeyType]ValueType
1
2
3
4
5
6
7
8
9
10
11
12
func main() {
scoreMap := make(map[string]int, 8)
scoreMap["butt3rf1y"] = 100
scoreMap["butt3rf2y"] = 90
fmt.Println(scoreMap)
fmt.Println(scoreMap["butt3rf2y"])
fmt.Printf("%T\n", scoreMap)
}
>>
map[butt3rf1y:100 butt3rf2y:90]
90
map[string]int

map 类型的变量默认初始值为 nil,需要用 make() 函数来分配内存

1
make(map[KeyType]ValueType,[cap])

接收两个参数,类型与初始容量

1
2
mp1 := make(map[string]int, 8)
mp2 := make(map[string][]int, 10)

map 数据都是成对出现的。支持在声明的时填充元素

1
2
3
4
5
6
7
8
9
10
11
func main() {
mp := map[int]string{
0: "b",
1: "u",
2: "t",
3: "3",
}
fmt.Println(mp)
}
>>
map[0:b 1:u 2:t 3:3]

访问

map 的访问和数组通过索引访问差不多,通过 Key 访问 Value

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
mp := map[string]int{
"a": 0,
"b": 1,
"c": 2,
"d": 3,
}
fmt.Println(mp["a"])
fmt.Println(mp["b"])
fmt.Println(mp["c"])
fmt.Println(mp["d"])
fmt.Println(mp["e"])
}

上面的代码中并不存在 e 的键值对,但是依然有返回值 0,map 对于不存在的键返回值是对应类型的零值,并且在访问 map 的时候其实有两个返回值,第一个返回对应类型的值,第二个返回值为布尔值,代表键是否存在,比如说

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {
mp := map[string]int{
"a": 0,
"b": 1,
"c": 2,
"d": 3,
}
if val, exist := mp["e"]; exist {
fmt.Println(val)
} else {
fmt.Println("不存在")
}
}
>>
不存在

求 map 长度用 len() 函数

1
len(mp)

存值

类似数组存值,存值时使用已存在的键会覆盖原有的值

1
2
3
4
5
6
7
8
9
10
11
func main() {
mp := make(map[string]int, 10)
mp["a"] = 1
mp["b"] = 2
if _, exist := mp["b"]; exist {
mp["b"] = 3
}
fmt.Println(mp)
}
>>
map[a:1 b:3]

但是当键为 math.NaN()

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
mp := make(map[float64]string, 10)
mp[math.NaN()] = "a"
mp[math.NaN()] = "b"
mp[math.NaN()] = "c"
_, exist := mp[math.NaN()]
fmt.Println(exist)
fmt.Println(mp)
}
>>
false
map[NaN:c NaN:a NaN:b]

相同的键值并没有被覆盖,而是并存。NaN 的实现是由底层的汇编指令 UCOMISD 完成的,这是一个无序比较双精度浮点数的指令。任何数字都不等于 NaN,NaN 也不等于自身,每次哈希值就不同,所以不要用 NaN 作为 map 的键

delete()

删除键值对,用 delete() 函数

1
2
3
delete(map,key)
map:要删除的 map
key:要删除的键值对的 key

比如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
mp := map[string]int{
"a": 0,
"b": 1,
"c": 2,
"d": 3,
}
fmt.Println(mp)
delete(mp, "a")
fmt.Println(mp)
}
>>
map[a:0 b:1 c:2 d:3]
map[b:1 c:2 d:3]

如果值为 NaN,那么没办法删除键值对

遍历

for range 遍历 map

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main() {
mp := map[string]int{
"a": 0,
"b": 1,
"c": 2,
"d": 3,
}
for key, val := range mp {
fmt.Println(key, val)
}
}
>>
c 2
d 3
a 0
b 1

因为 map 是无序的,遍历出来也是无序的

按照指定顺序遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func main() {
var scoreMap = make(map[string]int, 200)
for i := 0; i < 100; i++ {
key := fmt.Sprintf("%02d", i)
value := rand.Intn(100) // 生成 0-99 的随机整数
scoreMap[key] = value
}
// 取出 map 的所有 key 存入切片 keys
var keys = make([]string, 0, 200)
for key := range scoreMap {
keys = append(keys, key)
}
// 对切片进行排序
sort.Strings(keys)
// 按照排序后的 key 遍历 map
for _, key := range keys {
fmt.Println(key, scoreMap[key])
}
}

结构体

结构体声明,和一般结构体声明一样

1
2
3
4
5
6
7
8
9
10
11
12
type 类型名 struct {
字段名 字段类型
字段名 字段类型

}

type Programmer struct {
Name string
Age int
Job string
Language []string
}

实例化

go 没有构造方法,初始化像 map 一样指定字段名称再初始化字段值

1
2
3
4
5
6
programmer:=Programmer{
Name:"butt3rf1y",
Age:18,
Job:"stu",
Language:[]string{"Go","Java"}
}

如果实例化过程比较复杂,可以使用函数来实例化

1
2
3
4
5
6
7
8
9
10
type Person struct {
Name string
Age int
Address string
Salary float64
}

func NewPerson(name string, age int, address string, salary float64) *Person {
return &Person{Name: name, Age: age, Address: address, Salary: salary}
}

就像构造函数,但是 go 不支持函数与方法重载,如果需要用多种方式实例化结构体,要么创建多个构造函数,要么使用 options 模式

options 模式

假设有一个下面的结构体

1
2
3
4
5
6
7
type Person struct {
Name string
Age int
Address string
Salary float64
Birthday string
}

声明一个 PersonOptions 类型,接收一个 *Person 类型的参数,必须是指针

1
type PersonOptions func(p *Person)

创建选项函数,一般是 With 开头,返回值为一个闭包函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func WithName(name string) PersonOptions {
return func(p *Person) {
p.Name = name
}
}
func WithAge(age int) PersonOptions {
return func(p *Person) {
p.Age = age
}
}
func WithAddress(address string) PersonOptions {
return func(p *Person) {
p.Address = address
}
}
func WithSalary(salary float64) PersonOptions {
return func(p *Person) {
p.Salary = salary
}
}

实际声明的构造函数签名,接受一个可变长的 PersonOptions 类型的参数

1
2
3
4
5
6
7
8
9
10
11
12
func NewPerson(options ...PersonOptions) *Person {
p:=&Person{}
for _,option:=range options{
option(p)
}
if p.Age<0 {
p.Age=0
}
......

return p
}

对于不同实例化的需求只需要一个构造函数就行,传入不同的 Options 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
func main()  {
p1:=NewPerson(
WithName("butt3rf1y"),
WithAge(18),
WithAddress("US"),
WithSalary(100.00),
)

p2:=NewPerson(
WithName("kikk"),
WithAge(19),
)
}

组合

结构体之间的关系通过组合来表示,有显式组合和匿名组合

显式组合

1
2
3
4
5
6
7
8
9
10
11
12
type Person struct {
name string
age int
}
type Student struct {
p Person
school string
}
type Employee struct {
p Person
job string
}

如果需要显式的指定字段 p

1
2
3
4
5
student:=Student{
p:Person{name:"jack",age:18},
school:"lili school",
}
fmt.Println(student.p.name)

匿名组合

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
type Person struct {
name string
age int
}
type Student struct {
Person
school string
}
type Employee struct {
Person
job string
}


func main() {
var user struct {
Name string
Age int
}
user.Name = "hhah"
user.Age = 18
fmt.Printf("%#v\n", user)
}
>>
struct { Name string; Age int }{Name:"hhah", Age:18}

匿名字段名称默认为类型名,调用者可以直接访问该类型的字段和方法

1
2
3
4
5
student:=Student{
Person:Person{name:"jack",age:18},
school:"lili school",
}
fmt.Println(student.name)

指针

结构体指针不需要解引用就能访问

1
2
3
4
5
6
7
8
9
10
11
func main() {
type Person struct {
name string
age int
}
p := &Person{
name: "hhah",
age: 18,
}
fmt.Println(p.age, p.name)
}

p.age 其实在编译时会转换为 (*p).name

标签

标签是结构体的元信息,可以在运行时通过反射的机制读取

1
`key1:"value1" key2:"value2"`

假如 Student 结构体每个字段定义 json 序列化时用 Tag:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type Student struct {
ID int `json:"id"`
Gender string
name string
}

func main() {
s1 := Student{
ID: 1,
Gender: "女",
name: "hhah",
}
data, err := json.Marshal(s1)
if err != nil {
fmt.Println("json marshal failed")
return
}
fmt.Printf("%s\n", data)
}
>>
{"id":1,"Gender":"女"}

方法和接收者

方法(Method)是作用于特定类型变量的函数,特定类型变量叫接收者 (Receiver)

1
2
3
func (Receiver变量 Receiver类型) 方法名(参数列表) (返回参数){
函数体
}

比如说

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type Person struct {
name string
age int8
}

func NewPerson(name string, age int8) *Person {
return &Person{
name: name,
age: age,
}
}
func (p Person) Dream() {
fmt.Printf("%s", p.name)
}

func main() {
p1 := NewPerson("hhah", 18)
p1.Dream()
}
>>
hhah

指针类型的接收者

由于指针的特性,调用方法时修改接收者指针的任意成员变量,方法结束后修改也有效。

1
2
3
4
5
6
7
8
9
10
11
12
func (p *Person) SetAge(newAge int8) {
p.age = newAge
}
func main() {
p1 := NewPerson("cc", 18)
fmt.Println(p1.age)
p1.SetAge(20)
fmt.Println(p1.age)
}
>>
18
20

一般需要修改接收者中的值或者如果某个方法使用了指针接收者时用指针类型接收者

值类型的接收者

当方法用于值类型接收者时,会在代码运行时将值复制一份。在值类型接收者的方法中可以获取接收者的成员值,但无法改变接收者本身

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func (p Person) SetAge(newAge int8) {
p.age = newAge

}

func (p Person) Dream() {

}

func main() {
p1 := NewPerson("hhah", 18)
p1.Dream()
fmt.Println(p1.age)
p1.SetAge(20)
fmt.Println(p1.age)
}
>>
18
18

任意类型添加方法

接收者可以是任意类型,不仅仅是结构体,任何类型都可以拥有方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type MyInt int	//将 int 定义为 MyInt 类型

//为 MyInt 添加一个 SayHello 方法
func (m MyInt) SayHello() {
fmt.Println("hello")
}
func main() {
var m1 MyInt
m1.SayHello()
m1 = 100
fmt.Printf("%#v %T\n", m1, m1)
}
>>
hello
100 main.MyInt

不能给别的包的类型定义方法

嵌套

一个结构体可以嵌套另一个结构体或结构体指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
type Address struct {
Province string
city string
}

type User struct {
Name string
Gender string
Address Address
}

func main() {
user1 := User{
Name: "hhah",
Gender: "w",
Address: Address{
Province: "四川",
city: "成都",
},
}
fmt.Printf("user1=%#v\n", user1)
}
>>
user1=main.User{Name:"hhah", Gender:"w", Address:main.Address{Province:"四川", city:"成都"}}

嵌套匿名结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type Address struct {
Province string
city string
}

type User struct {
Name string
Gender string
Address
}

func main() {
var user2 User
user2.Name = "cc"
user2.Gender = "w"
user2.Address.Province = "四川"
user2.city = "成都"
fmt.Printf("user2=%#v\n", user2)
}
>>
user2=main.User{Name:"cc", Gender:"w", Address:main.Address{Province:"四川", city:"成都"}}

内存对齐

结构体字段的内存分布遵循内存对齐的规则


Re:从零开始的 Go 之旅- 基础语法(3)
http://example.com/2025/03/09/Go-3/
作者
butt3rf1y
发布于
2025年3月9日
许可协议