20小时快速入门go语言视频 - Day3

20小时快速入门go语言视频 - Day3



一、复合类型有哪些

Pointer 指针
Array 数组
Slice 切片
Map 哈希表
Struct 结构体
Interface 接口
Channel 通道
func 函数类型



二、指针

指针是一个代表着某个内存地址值的数据类型,这个内存地址往往是在内存中存储的另一个变量的值的起始位置。
每个变量有2层含义:变量的内容,变量的地址。

2.1 指针最基本的操作

2.1.1 Golang指针的特点

1.默认值为 nil
2.操作符 & 取变量的地址,* 操作内存地址中的内容(值)。
3.不支持指针运算,使用 . 访问目标成员。

2.1.2 基本例子

定义指针类型的语法:var 变量名 *数据类型*星号千万不要忘了,不然就是一个普通的数据类型,而不是指针类型!
取出内存地址的值,赋值给指针类型的语法:指针的变量 = &变量名。这步操作就是让指针指向合法内存地址。&不要忘了,不然就是普通的值赋值给指针类型,两者数据类型不匹配肯定报错!

func main() {
	var a = 10
	fmt.Printf("a=%d\n", a)   //这里是变量的内存,内存里的内容
	fmt.Printf("&a=%p\n", &a) //内存外的标号,变量的地址,也叫指针

	//保存某个变量的地址,需要指针类型
	var p *int //保存int的地址
	p = &a     //指针变量想要指向谁,就把它的地址赋值给指针变量。这里想要指向上面的变量a,就取出a的地址赋值给指针变量即可
	fmt.Printf("p=%v, &a=%v\n", p, &a)
	//p=0xc00000a0b8, &a=0xc00000a0b8 值一样,因为:p就是保存了a的内存地址,&a是取出a的地址

	*p = 666                           //*p不是操作p的地址,而是操作p所指向的那个内存中的值。这里是给所指向的a赋值,就是操作a中的值
	fmt.Printf("*p=%v, a=%v\n", *p, a) //*p=666, a=666
}

/*
运行结果:
a=10
&a=0xc00000a0b8
p=0xc00000a0b8, &a=0xc00000a0b8
*p=666, a=666
*/

指针指向谁,就把谁的内存地址赋值给指针!指针一定要指向一个合法的内存地址!
大致流程图:
在这里插入图片描述

2.1.3 一些说明

1.变量都存放在内存当中。
2.每个变量在内存中有一个标号,也就是变量的地址(内存地址),也叫指针。使用取地址符 &,找到变量的标号(内存地址)。
3.想要保存这个标号(内存地址),就需要用到指针类型。
4.不要操作没有合法指向的内存
错误示例:

func main() {
	var p *int //声明变量p为int指针类型
	//没有指向内存地址的时候,值为nil
	fmt.Println("p=", p) //p= <nil>

	*p = 666
	/*
	panic: runtime error: invalid memory address or nil pointer dereference
	[signal 0xc0000005 code=0x1 addr=0x0 pc=0x49a833]
	无效的内存地址或nil指针引用
	*/
}

5.数据类型要一样!不要定义的指针变量是 int,但最终却指向了其他类型。
错误示例:

func main() {
	var a int
	var p *float64

	p = &a //报错:cannot use &a (type *int) as type *float64 in assignment ===> 不能在分配中使用&a(类型* int)作为类型* float64
}

6.操作指针所指向的变量时,不要忘了在前面带上星号 ** 操作的是内存地址中的值。

func main() {
	var a int
	var p *int //声明一个p变量,类型是int指针

	p = &a  //p保存了a的地址,内存地址是个指针类型
	p = 111 //p前面的星号不要忘了,否则就报错:cannot use 111 (type int) as type *int in assignment ===> int类型不能赋值给指针类型

	fmt.Printf("a=%v, p=%v", a, p)
}

2.2 new()函数的使用

new(Type) 内置函数分配内存。Type 是一个具体写明的数据类型,而不是值。返回值是指向该类型新分配的零值的指针。
new(Type) 创建了一个匿名指针变量,为新值分配一块内存空间,其值为该类型的零值,然后将这块内存空间的地址作为结果返回。
之前的合法指向写法是声明一个变量和声明一个同类型的指针变量,然后将变量的内存地址赋值给指针变量,再把值赋值给带 * 的指针变量。new(Type) 是合法指向的另一种写法,指向一个没有名字的内存,实则是动态分配内存空间。

func main() {
	var p *int   //int指针类型,指向int类型
	p = new(int) //p指针类型指向的是一个int类型,所以new()小括号里也必须是同类型
	*p = 111
	fmt.Printf("p=%v, *p=%v\n", p, *p) //p=0xc000062090, *p=111

	n := new(string) //自动推导类型,省略了声明的步骤,new(type)返回这个类型的零值
	*n = "abce"
	fmt.Printf("n=%v, *n=%v\n", n, *n) //n=0xc0000341f0, *n=abce
}

在这里插入图片描述

2.3 值传递

值传递,把值拷贝一份过去,传递的是该值的副本,自身是不会被改变的。
Golang 中,除了 mapslicechan,其他都是值传递。

func swap(a, b int) {
	a, b = b, a
	fmt.Printf("swap: a=%v, b=%v\n", a, b) //swap: a=20, b=10
	//swap里面a,b交换了
}

func main() {
	a, b := 10, 20
	swap(a, b)                             //站在变量的角度,变量本身传递过去是属于值传递
	fmt.Printf("main: a=%v, b=%v\n", a, b) //main: a=10, b=20
	//main里面依然没有交换
}

main() 中,a, b 是整型,传递的时候属于值传递。

2.4 使用指针进行地址传递

将变量的地址传递过去,俗称的"传址调用"。
示例:

func swap(p1, p2 *int) {
	*p1, *p2 = *p2, *p1 //星号"*"直接操作内存地址中的值,星号"*"就是指针指向的那块内存
	fmt.Printf("swap: p1=%v, p2=%v\n", *p1, *p2)
}

/*
func swap(p1, p2 *int) { //接收指针类型的实参
	p1, p2 = p2, p1 //没带星号"*"只是交换了各自的内存地址的值(0xc0000a0..)
	fmt.Printf("swap: p1=%v, p2=%v\n", *p1, *p2)
	//swap: p1=0xc00000a0d0, p2=0xc00000a0b8
}
*/

func main() {
	a, b := 10, 20
	swap(&a, &b)
	fmt.Printf("a address:%v, b address:%v\n", &a, &b)
	fmt.Printf("main: a=%v, b=%v\n", a, b)
}

/*
运行结果:
swap: p1=20, p2=10
a address:0xc00000a0b8, b address:0xc00000a0d0
main: a=20, b=10
*/



三、数组

数组是指一系列同一类型数据的集合。作用:很多元素都是同一数据类型,放进一个集合中更容易操作和管理。
数组中包含的每个数据被称为数组元素(element),一个数组包含的元素个数被称为数组的长度。
数组长度必须是常量,因为数组的长度不可变,是其声明的组成部分。[2]int[3]int 是不同类型!

3.1 声明语法

语法:var 名称 [N]Type
N 指定数组长度的一个常量,或者是一个固定的 int 值。
Type 是这个数组中元素的数据类型,数组中的所有元素的类型必须都一样。
[] 不能忘了写,不然就是其他类型而不是数组类型了。
示例:

var n = 10
var a [n]int  //非法定义:non-constant array bound n, Invalid array bound 'n', must be a constant expression
var b [10]int //合法定义:代表这个数组长度为10

const num = 20
var c [num]string //合法定义:num是一个常量

3.2 初始化数组

初始化就是定义的同时并赋值,数组的长度是固定的,初始化的时候必须写明长度

3.2.1 传统写法

语法:var variableName [const]Type = [const]Type{/*放入const个元素*/}
其中等号 = 左边的 [const]Type 可以省略不写,变成:var 名称 = [const]Type{/*放入const个元素*/}
自动推导:名称 := [const]Type{/*放入const个元素*/}

3.2.2 部分初始化

没有初始化的元素,自动赋值为其类型的零值。

3.2.3 省略号 ... 出现在数组长度的位置

在初始化数组中,如果省略号 ... 出现在数组长度的位置,那么数组的长度由初始化数组的元素个数所决定。注意:它依然是数组
例1:... 的长度由元素个数来决定了

func main() {
	q := [...]int{1, 2, 3, 4, 5}

	fmt.Printf("q length:%d, q type is:%[2]T, q=%[2]v\n", len(q), q)
	//q length:5, q type is:[5]int, q=[1 2 3 4 5]
}

例2:没有元素的话,长度就为 0 了

func main() {
	q := [...]int{}

	fmt.Printf("q length:%d, q type is:%[2]T, q=%[2]v\n", len(q), q)
	//q length:0, q type is:[0]int, q=[]
}
3.2.4 指定下标初始化

通过指定下标来给值,无需按顺序给出一组值了:

func main() {
	//指定下标,然后给值。无需再按照顺序逐个一一对应地给值了
	symbol := [...]string{3: "¥", 2: "£", 1: "€", 0: "$"}

	fmt.Printf("USD:%v\n", symbol[0])
	fmt.Printf("EUR:%v\n", symbol[1])
	fmt.Printf("GBP:%v\n", symbol[2])
	fmt.Printf("RMB:%v\n", symbol[3])
}

/*
运行结果:
USD:$
EUR:€
GBP:£
RMB:¥
*/

常量生成器 iota 的值是从 0 开始,每行的值递增 1。
运用常量生成器 iota 的特性,来实现指定下标给值。上例中,数组初始化那条语句,也可以写成下面等价的语句:

func main() {
	type Currency int

	const (
		USD Currency = iota //iota 从 0 开始递增 1
		EUR
		GBP
		RMB
	)

	symbol := [...]string{RMB: "¥", GBP: "£", EUR: "€", USD: "$"} //常量是 iota 生成器,iota 从 0 开始递增 1,所以也可以当做下标来使用

	fmt.Printf("USD:%v\n", symbol[USD]) //iota 当下标来使用
	fmt.Printf("EUR:%v\n", symbol[EUR])
	fmt.Printf("GBP:%v\n", symbol[GBP])
	fmt.Printf("RMB:%v\n", symbol[RMB])
}

/*
运行结果:
USD:$
EUR:€
GBP:£
RMB:¥
*/
3.2.5 综合示例
func main() {
	//只声明不赋值
	//var a [3]int

	//初始化:定义的同时给与赋值

	//全部初始化
	var a = [5]int{1, 2, 3, 4, 5} //等号左边的 [5] int,可以省略不写
	fmt.Println(a)

	//自动推导
	b := [5]int{111, 222, 333, 444, 555}
	fmt.Printf("b type is : %T, b:%v\n", b, b)

	//部分初始化,没有初始化的元素自动赋值给其对应类型的零值
	c := [5]int{777, 100}
	fmt.Println("c=", c)

	//指定某个下标中的值(也叫指定初始化)
	d := [5]int{2: 999, 4: 666}
	fmt.Println("d=", d)
}

/*
运行结果:
[1 2 3 4 5]
b type is : [5]int, b:[111 222 333 444 555]
c= [777 100 0 0 0]
d= [0 0 999 0 666]
*/
3.2.6 小技巧
3.2.6.1 快速声明一个长度为50的数组

指定最后一个下标的值,其他元素的值都是该指定类型的零值。

func main() {
	r := [...]int{49: 0} //指定最后一个下标的值,其他元素都是 int 类型的零值

	fmt.Printf("r length:%d, r type is%[2]T, r=%[2]v\n", len(r), r)
	//r length:50, r type is[50]int, r=[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 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
}

3.3 二维数组

定义时有多少个方括号 [] 就是多少维数组。二维数组相当于一个表格,[0,0] 下标从最左上角开始。

func main() {
	//有2个方括号"[]",就是二维数组
	var a [3][4]int //3行4列的数组

	//赋值,有多少个"[]"就用多少层循环
	k := 111
	for i := 0; i < 3; i++ {
		for j := 0; j < 4; j++ {
			k++
			a[i][j] = k
			fmt.Printf("a[%d][%d] = %d, ", i, j, a[i][j])
		}
		fmt.Println()
	}
	fmt.Println("a=", a)

	//初始化一个二维数组
	//第一个方括号"[]"代表有几行,第二个方括号"[]"代表有几列
	b := [3][3]int{
		{1, 2, 3}, //使用大括号包裹起来
		{4, 5, 6},
		{7, 8, 9}, //逗号不要漏了,这是语法规定
	}
	fmt.Println("b=", b)

	//指定初始化,未初始化的是对应数据类型的零值
	//4行3列,只初始化第3行的数据
	c := [4][3]byte{2: {'g', 'p', 'r'}}
	fmt.Println("c=", c)
}

/*
运行结果:
a[0][0] = 112, a[0][1] = 113, a[0][2] = 114, a[0][3] = 115,
a[1][0] = 116, a[1][1] = 117, a[1][2] = 118, a[1][3] = 119,
a[2][0] = 120, a[2][1] = 121, a[2][2] = 122, a[2][3] = 123,
a= [[112 113 114 115] [116 117 118 119] [120 121 122 123]]
b= [[1 2 3] [4 5 6] [7 8 9]]
c= [[0 0 0] [0 0 0] [103 112 114] [0 0 0]]
*/

另一种实现二维数组的方式:

func main() {
	h, w := 2, 4

	raw := make([]int, h*w)
	for i := range raw {
		raw[i] = i
	}
	fmt.Println("raw =", raw)

	table := make([][]int, h)
	for i := range table {
		table[i] = raw[i*w : i*w+w]
	}
	fmt.Println("table =", table)
}

/*
运行结果:
raw = [0 1 2 3 4 5 6 7]
table = [[0 1 2 3] [4 5 6 7]]
*/

备注:示例来源:https://studygolang.com/articles/28753(创建一个动态的多维数组需要三步)

3.4 数组比较和赋值

3.4.1 数组比较的原则

如果数组中的元素类型是可比较的,那么这个数组也是可比较的。比较的结果是两个数组中,所有的元素的值是否完全相同。
数组只支持 ==!=,不能用小于等于。

3.4.2 数组比较的示例

例1:

func main() {
	a := [5]int{1, 2, 3, 4, 5}
	b := [5]int{1, 2, 3, 4, 5}
	c := [5]int{1, 2, 3}
	fmt.Println("a == b ?", a == b)
	fmt.Println("b == c ?", b == c)
}

/*
运行结果:
a == b ? true
b == c ? false
*/

例2:

func main() {
	a := [2]int{1, 2}
	b := [...]int{1, 2} //...的长度由数组中的元素个数来决定。这里其实就是 [2]int 类型
	c := [2]int{1, 3}
	fmt.Println(a == b, a == c, b == c)

	d := [...]int{1, 3} //这里其实就是 [2]int 类型
	fmt.Println(a == d, b == d, c == d) //比较的原则是两边元素的值是否完全相同,a d 两个数组中,第二个元素的值不同,所以 == 比较的结果为 false
}

/*
运行结果:
true false false
false false true
*/
3.4.3 数组相互赋值

只有当长度相同且为同类型的数组之间,才可以相互赋值。

func main() {
	a := [5]int{1, 2, 3, 4, 5}
	fmt.Printf("a type is:%T\n", a)

	var d [5]int
	fmt.Printf("d type is:%T\n", d)

	d = a //长度和类型都一样的数组才能相互赋值

	fmt.Println("d=", d)

	a = [5]int{10, 9, 8, 7, 6}
	fmt.Println("a=", a)
}

/*
运行结果:
a type is:[5]int
d type is:[5]int
d= [1 2 3 4 5]
a= [10 9 8 7 6]
*/

不同长度的数组是不同的类型,就算它们的数据类型一样也不行。
总之一句话:数组长度不同、或者数据类型不同,都不是同一类型的数组
反面示例:

func main() {
	a := [3]int{1, 2, 3}
	b := [5]int{1, 2, 3, 4, 5}
	fmt.Println("a == b?", a == b) //invalid operation: a == b (mismatched types [3]int and [5]int) ===> 不匹配类型[3]int和[5]int
}
3.4.4 注意事项

1.Golang 压根就没设计过数组之间 < <= > >= 的运算,只支持 ==!=

func main() {
	a := [5]int{1, 2, 3}
	b := [5]int{1, 2, 3, 4, 5}
	fmt.Println("a == b?", a < b) //invalid operation: a < b (operator < not defined on array)
}

3.5 数组做函数参数,值传递(值拷贝)

值传递是把整个数组拷贝一份传递给形参,形参接收的是实参的一份副本
注意:当数组中元素数量很多的时候,值拷贝的效率非常低下,而且可能会爆内存

//注意:参数类型要一模一样
func test1(a [5]int) {
	a[0] = 111111
	fmt.Println("test1: a=", a)
}

func main() {
	a := [5]int{1, 2, 3, 4, 5} //初始化
	test1(a)
	fmt.Println("main: a=", a)
}

/*
运行结果:
test1: a= [111111 2 3 4 5]
main: a= [1 2 3 4 5]
*/

3.6 数组做函数参数,引用传递(传址)

两个函数需要共用一个数组,或者这个数组中元素数量非常多,不适合值传递的时候,就可以用到数组指针。
注意:使用数组指针后,任何修改都将影响到原本的数组。
示例:

func test1(a *[5]int) { //数组指针,指向实参a的内存地址
	a[0] = 1111111111
	fmt.Println("test1: a=", *a) //取值用*
}

func main() {
	a := [5]int{1, 2, 3, 4, 5}
	test1(&a) //地址传递,变量前加一个取地址符&
	fmt.Println("main: a=", a)
}

/*
运行结果:
test1: a= &[1111111111 2 3 4 5]
main: a= [1111111111 2 3 4 5]
*/



四、Slice 切片

数组的长度在定义后就无法修改,而且数组是值类型。切片通过内部指针和相关属性引用底层数组的片段,以实现变长方案。
Slice 本质上不是一个数组,而是一个引用类型,只是一个指向底层数组的指针。
简单理解,可以把 Slice 称呼为:“动态数组”。

4.1 切片初始化

1.a := []数据类型{},自动推导类型。注意:[]中不要写具体的数值,否则它的类型就是数组了。
2.a := make([]数据类型, 长度, 容量),长度必须写!容量可以省略不写,当容量不写的时候,跟长度的值保持一样。

4.2 基本示例

示例:

func main() {
	a := [6]int{0, 1, 2, 3, 4, 5} // []里写了数字就是一个数组
	s := a[0:3]
	fmt.Printf("s type is : %T, s=%v\n", s, s)
	fmt.Printf("len(s)=%v, cap(s)=%v\n", len(s), cap(s))

	fmt.Println("--------------------------")

	s = a[2:4] // 从数组a下标2开始引用,4-2=2,引用2个元素。因为只引用2个元素,数组中剩余的元素就被隐藏起来了
	fmt.Println("s = a[2:4] ===> ", s)
	fmt.Println("len(s)=", len(s))
	fmt.Println("cap(s)=", cap(s)) //从数组a下标2开始引用,cap只计算下标2开始到最后的"长度"

	// 切片是对数组的引用,这里的切片s引用的是数组a,所以对s的append也会同时append到a中去
	// append() 是末尾添加,当前切片s的末尾元素的值是3,在元素3后面添加新元素
	// 同样也将从数组a当中的元素3后面开始添加(因为左闭右开)
	s = append(s, 111)

	s = append(s, 222)
	s = append(s, 333) // a的cap达到极限了,无法再继续append
	s = append(s, 444) // "动态数组"会自动增加容量
	fmt.Println("s=", s)
	fmt.Println("a=", a)
}

/*
运行结果:
s type is : []int, s=[0 1 2]
len(s)=3, cap(s)=6
--------------------------
s = a[2:4] ===>  [2 3]
len(s)= 2
cap(s)= 4
s= [2 3 111 222 333 444]
a= [0 1 2 3 111 222]
*/

4.3 切片截取(运算符 slice[i:j]

使用切片运算符 slice[i:j],创建另一个切片。
语法:varName[low:high:max]
长度概念:len=high-low
容量概念:cap=max-low
在这里插入图片描述

4.4 append() 函数

内建函数 append(),只能用于 Slice 类型!
append() 在原 Slice 的末尾追加元素,并返回一个新的切片,当切片容量不够时会自动扩容。(通常以2倍增加)
append() 自动扩容机制:一旦超过原底层数组的容量,append() 时就会自动增加底层数组的容量,通常以2倍容量重新分配底层数组,并复制原来的数据。
append() 扩容示例:

func main() {
	s1 := make([]int, 0, 0)
	oldCap := cap(s1)
	fmt.Printf("len(s1)=%v, cap(s1)=%v\n", len(s1), oldCap)

	for i := 0; i < 20; i++ {
		s1 = append(s1, i) //容量不够时,通常会以2倍容量进行扩容
		if newCap := cap(s1); oldCap < newCap {
			fmt.Printf("cap change: oldCap=%d ===> newCap=%d\n", oldCap, newCap)
			oldCap = newCap
		} else {
			fmt.Println("sufficient capacity. not necessarily allocate capacity.")
		}
	}
}

/*
运行结果:
len(s1)=0, cap(s1)=0
cap change: oldCap=0 ===> newCap=1
cap change: oldCap=1 ===> newCap=2
cap change: oldCap=2 ===> newCap=4
sufficient capacity. not necessarily allocate capacity.
cap change: oldCap=4 ===> newCap=8
sufficient capacity. not necessarily allocate capacity.
sufficient capacity. not necessarily allocate capacity.
sufficient capacity. not necessarily allocate capacity.
cap change: oldCap=8 ===> newCap=16
sufficient capacity. not necessarily allocate capacity.
sufficient capacity. not necessarily allocate capacity.
sufficient capacity. not necessarily allocate capacity.
sufficient capacity. not necessarily allocate capacity.
sufficient capacity. not necessarily allocate capacity.
sufficient capacity. not necessarily allocate capacity.
sufficient capacity. not necessarily allocate capacity.
cap change: oldCap=16 ===> newCap=32
sufficient capacity. not necessarily allocate capacity.
sufficient capacity. not necessarily allocate capacity.
sufficient capacity. not necessarily allocate capacity.
*/

4.5 copy() 函数

内建函数 copy(),只能用于 Slice 类型!
在两个 Slice 间复制数据,复制长度以 len() 小的为准,两个 Slice 可指向同一底层数组。
示例:

func main() {
	srcSlice := []int{1, 2}
	dstSlice := []int{7, 8, 9, 10}

	n := copy(dstSlice, srcSlice) //相当于把srcSlice替换到dstSlice的开始处
	fmt.Println("dstSlice=", dstSlice)
	fmt.Printf("how much copied:%d\n", n)
	fmt.Println("------------------------------------------")

	srcSlice = []int{1, 2, 3, 4, 5}
	dstSlice = []int{77, 88}
	n = copy(dstSlice, srcSlice) //目标切片只有2个容量,所以只有2个被copy过去,其余都丢弃
	fmt.Println("dstSlice=", dstSlice)
	fmt.Printf("how much copied:%d\n", n)
	fmt.Println("------------------------------------------")

	srcSlice = []int{1, 2, 3, 4, 5}
	dstSlice = make([]int, 10) //剩余的元素的值使用默认类型的零值
	n = copy(dstSlice, srcSlice)
	fmt.Println("dstSlice=", dstSlice)
	fmt.Printf("how much copied:%d\n", n)
}

/*
运行结果:
dstSlice= [1 2 9 10]
how much copied:2
------------------------------------------
dstSlice= [1 2]
how much copied:2
------------------------------------------
dstSlice= [1 2 3 4 5 0 0 0 0 0]
how much copied:5
*/

注意:使用切片运算符 slice[i:j] 创建另一个切片,并对新切片中的元素进行更改时,原始切片也将发生变化。因为切片只是一个指向底层数组的指针,如果不希望影响到其他切片,就需要使用这个 copy() 函数创建切片的副本。

4.6 切片作为函数参数是引用传递(传址)

切片作为函数的参数,采用引用传递(传址),不会像数组那样把整个都拷贝一份传递过去,切片只是把它的内存地址传递过去。
由于切片是引用传递(传址),所以传参时没必要用到指针,也能实现同时修改。
示例:

import (
	"fmt"
	"math/rand"
	"time"
)

//初始化切片
func InitSlice(s []int) { //参数是int类型的切片
	rand.Seed(time.Now().UnixNano()) //以当前时间的纳秒作为种子

	for i := 0; i < 10; i++ {
		s[i] = rand.Intn(100) //0~100的随机正整数
	}
}

//冒泡排序
func Bubbling(s []int) { //参数是int类型的切片
	sLen := len(s)
	for i := 0; i < sLen; i++ {
		for j := 0; j < sLen-i-1; j++ {
			if s[j] > s[j+1] {
				s[j], s[j+1] = s[j+1], s[j]
			}
		}
	}
}

func main() {
	n := 10
	s := make([]int, n) //声明一个长度为n的int类型切片

	InitSlice(s)

	fmt.Println("before store, s=", s)

	Bubbling(s)

	fmt.Println("after store, s=", s)
}

/*
运行结果:
before store, s= [48 63 24 35 4 0 41 21 61 78]
after store, s= [0 4 21 24 35 41 48 61 63 78]
*/

4.7 slice 只允许和 nil 做比较

slice 类型的零值为 nilslice 类型只允许和 nil 做比较

4.7.1 nil 表示没有内存空间

slice 数据类型是引用类型,只有当不分配内存空间时,才为 nil

func main() {
	var s1 []int //只是声明,不会去申请内存空间
	fmt.Printf("s1 == nil ? %t, addr:%p, len(s1)=%d\n", s1 == nil, s1, len(s1))

	s1 = nil //手动显示地赋值了 nil,那么肯定是 nil
	fmt.Printf("s1 == nil ? %t, addr:%p, len(s1)=%d\n", s1 == nil, s1, len(s1))

	//切片的值可以为 nil,可以写成转换表达式 []int(nil)
	s1 = []int(nil) //[]int{nil} 会报错,因为 int 的零值不是 nil
	fmt.Printf("s1 == nil ? %t, addr:%p, len(s1)=%d\n", s1 == nil, s1, len(s1))

	s1 = []int{} //赋值了,只是它是一个没有元素的空切片,赋值会去申请开辟内存空间
	fmt.Printf("s1 == nil ? %t, addr:%p, len(s1)=%d\n", s1 == nil, s1, len(s1))

	s2 := []int{} //短变量就是声明+赋值,就是初始化,初始化了就会去申请内存空间
	fmt.Printf("s2 == nil ? %t, addr:%p, len(s2)=%d\n", s2 == nil, s2, len(s2))

	s3 := []int(nil)
	fmt.Printf("s3 == nil ? %t, addr:%p, len(s3)=%d\n", s3 == nil, s3, len(s3))

	//make() 函数会返回一个具体的类型,只有是引用类型才能使用 make() 函数,既然引用了,那么这个类型就已经存在于内存中
	s4 := make([]int, 0)
	fmt.Printf("s4 type is:%T, s4 == nil ? %t, addr:%p, len(s4)=%d\n", s4, s4 == nil, s4, len(s4))

	//具体类型赋值给了变量,这个变量其实是引用类型了,那么就有了内存空间
	var s5 = make([]int, 0)
	fmt.Printf("s5 type is:%T, s5 == nil ? %t, addr:%p, len(s5)=%d\n", s5, s5 == nil, s5, len(s5))
}

/*
运行结果:
s1 == nil ? true, addr:0x0, len(s1)=0
s1 == nil ? true, addr:0x0, len(s1)=0
s1 == nil ? true, addr:0x0, len(s1)=0
s1 == nil ? false, addr:0x5a7da8, len(s1)=0
s2 == nil ? false, addr:0x5a7da8, len(s2)=0
s3 == nil ? true, addr:0x0, len(s3)=0
s4 type is:[]int, s4 == nil ? false, addr:0x5a7da8, len(s4)=0
s5 type is:[]int, s5 == nil ? false, addr:0x5a7da8, len(s5)=0
*/
4.7.2 这些情况下,slice 的值为 nil
4.7.2.1 var s []int 形式的声明

var s []int 只是声明,没有给值(没有赋值),不会去申请内存空间。

4.7.2.2 s = []int(nil) 转换表达式

因为 slice 的值可以为 nil,所以可以使用这个转换表达式:[]int(nil)

4.7.2.3 手动赋值为 nil
func main() {
	s := []int{}
	s = nil
	fmt.Printf("s == nil ? %t, addr:%p, len(s)=%d\n", s == nil, s, len(s))
	// s == nil ? true, addr:0x0, len(s)=0
}

手动给了 nil,那么肯定为 nil 了。

4.8 对 Slice 进行截取时,起始下标刚好等于长度

Slice 进行截取时,起始位置的下标刚好等于这个 Slice 的长度。此时,是没有任何语法错误的,只是结果是一个空切片。
演示:

func main() {
	s := []int{0, 1, 2, 3, 4}
	fmt.Printf("len(s)=%[1]d, cap(s)=%[1]d, type:%[1]T\n", s[5:])
	// len(s)=[], cap(s)=[], type:[]int
}

在 Golang 中,对 Slicestring 进行范围截取(也就是 [m:n])时。mn 的值,只要在这个范围内:0 <= m <= n <= len(s),就不会发生任何错误、异常。
大致上的原因个人分析:
以上面这个代码片段为例。Slice 变量 s 中,有 5 个元素,长度为 5。在 Golang 中,上届 n 和下届 m,他们是互斥的。能够截取多少个元素是由 n - m 所决定的。代码片段中,进行了 s[5:] 的操作。此时,上届没有指定,那么默认就是 5,因为 s 的长度为 5,上届已经指定了是 5。上届和下届都是在 0 <= m <= n <= len(s) 这个范围中,符合规范。5 - 5 = 0,只能取到 0 个元素,所以最终是个空切片。
另外注意:上届可以超过长度,但不能超过容量!
我的理解是基于 stackoverflow 上的这个解答:link

4.9 切片与数组的区别和关系

4.9.1 区别:长度

数组:数组的长度是固定写死的,方括号 [] 中必须写明常量,例:a := [5]int{}
切片:方括号 [] 中不写或者 ... 来代替,例:a := [...]int{}

4.9.2 区别:做函数调用时

数组是按值传递(传值),切片是按引用传递(传址)。

4.9.3 区别:只有 Slice 可以使用 append() 函数

append() 的第一个参数必须切片类型。
示例:

func main() {
	a := [6]int{0, 1, 2, 3, 4, 5}
	a = append(a, 111111) // first argument to append must be slice; have [6]int
	fmt.Println("a=", a)
}

数组的长度是固定不能修改的,append() 将会改变数组的长度,自相矛盾了。

4.9.4 关系:切片是对底层数组的引用

引用表示共用同一个内存地址
例1:

func main() {
	a := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}                          // 这是数组
	s1 := a[2:5]                                                        // 这是切片
	fmt.Printf("s1=%v, len(s1)=%v, cap(s1)=%v\n", s1, len(s1), cap(s1)) // s1=[2 3 4], len(s1)=3, cap(s1)=8
	fmt.Println("--------------------------")

	// 引用表示的是它们同时在使用同一个内存地址,无论哪一个修改,都是在直接修改内存地址
	// 由于是使用同一个内存地址,所以最终的值都是一样

	s1[1] = 9999           // 切片是对数组的引用,修改切片中的元素,将会影响到底层的数组
	fmt.Println("s1=", s1) // s1= [2 9999 4]
	fmt.Println("a=", a)   // a= [0 1 2 9999 4 5 6 7 8 9]
	fmt.Println("--------------------------")

	a[2] = 100000 // 数组修改元素后,切片是引用了数组,一样也会改变切片的元素
	fmt.Println("s1=", s1)
	fmt.Println("a=", a)
}

/*
运行结果:
s1=[2 3 4], len(s1)=3, cap(s1)=8
--------------------------
s1= [2 9999 4]
a= [0 1 2 9999 4 5 6 7 8 9]
--------------------------
s1= [100000 9999 4]
a= [0 1 100000 9999 4 5 6 7 8 9]
*/

例2:

func main() {
	a := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

	s1 := a[2:5] //切片,从数组下标2开始引用3个元素,cap=len(a)-2
	fmt.Printf("s1=%v, len(s1)=%v, cap(s1)=%v\n", s1, len(s1), cap(s1))
	fmt.Println("---------------------------------------")

	s2 := s1[2:8] //此时s1的cap为8,所以取下标时不要越界了。这里从切片s1的下标2开始取6个元素
	fmt.Printf("before change: s1=%v\n", s1)
	fmt.Printf("before change: s2=%v\n", s2)
	fmt.Println("---------------------------------------")

	s2[3] = 7777777
	fmt.Printf("after changed: s1=%v\n", s1)
	fmt.Printf("after changed: s2=%v\n", s2)
	fmt.Println("a=", a)
}

/*
运行结果:
s1=[2 3 4], len(s1)=3, cap(s1)=8
---------------------------------------
before change: s1=[2 3 4]
before change: s2=[4 5 6 7 8 9]
---------------------------------------
after changed: s1=[2 3 4]
after changed: s2=[4 5 6 7777777 8 9]
a= [0 1 2 3 4 5 6 7777777 8 9]
*/

在这里插入图片描述

4.10 语法糖 ...

... 作为 Go 语言的语法糖,主要有 2 个用途:作为函数的不定长参数,打散切片。

4.10.1 作为函数的不定长参数

作为函数不定长参数的使用,已在 Day2 笔记中有记录。前往笔记

4.10.2 打散切片

示例:

func main() {
	src := []int{1, 2, 3}
	dst := []int{4, 5, 6}

	fmt.Printf("before append, src = %v\n", src)

	src = append(src, dst...)

	fmt.Printf("after append, src = %v\n", src)
}

/*
运行结果:
before append, src = [1 2 3]
after append, src = [1 2 3 4 5 6]
*/

本质上是因为 append() 函数的第二个参数,它是某个数据类型的不定长参数。不定长参数需要接受的是具体的类型,而不是一个切片。因此,使用 ... 将切片打散后,就变成了一个个具体的数据类型,那么就符合了 append() 函数参数的类型。


五、map

底层使用哈希表 HashMap 实现的无序的 key-value 键值对的集合,一个 map 中所有的** key 是唯一的**。
key 必须是支持 ==!= 操作符的数据类型。切片、函数以及包含切片的结构类型由于具有引用语义,不能作为 mapkey
value 可以是任意类型,没有限制。map 里所有 key 的数据类型必须是相同的,value 的数据类型也必须都是相同的。但 keyvalue 的数据类型可以是不同的。

5.1 声明、初始化 map

5.1.1 map[key数据类型]value数据类型

语法格式:map[keyType]valueType

func main() {
	var m1 map[int]string  // 声明一个 map,但没有给值(没有初始化),所以是个空 map
	fmt.Println("m1=", m1) // m1= map[]
}
5.1.2 make() 函数

语法格式:varName := make(map[keyType]valueType, len)len 可以省略不写,map 是根据元素数量自动对 len 进行扩容。
注:如果明确知道了这个 map 会有多少个元素,最好把 len 写上。因为它在 make() 的时候就把对应长度的内存空间申请下来了,不用每次都去检测、自动扩容,提高了效率。
示例:

func main() {
	m1 := make(map[int]string) //map可以不指定长度,因为它会自动扩容
	fmt.Println("m1=", m1)

	m2 := make(map[int]string, 2)
	fmt.Printf("m2=%v, len(m2)=%d\n", m2, len(m2)) //map中的len是计算元素的数量
	m2[0] = "go" //对map的操作
	m2[1] = "python"
	m2[2] = "rust"
	fmt.Printf("m2=%v, len(m2)=%d\n", m2, len(m2)) //map会对len自动扩容
}

/*
运行结果:
m1= map[]
m2=map[], len(m2)=0
m2=map[0:go 1:python 2:rust], len(m2)=3
*/

另外要注意一点:make() 中的 len 属性可以指定 map 的长度,但无法获得他的 cap
错误示例:(这段代码编译都编不过,编译时错误)

func main() {
	m := make(map[string]int, 2)
	m["a"] = 1
	m["b"] = 2
	m["c"] = 3
	m["d"] = 4
	fmt.Println("cap(m) =", cap(m)) // invalid argument m (type map[string]int) for cap
}
5.1.3 初始化 map 的方式

键值对的写法:key:value

func main() {
	m1 := map[int]string{1: "python", 2: "rust", 0: "go"} //每个元素都是key:value的写法
	fmt.Println("m1=", m1)

	m2 := map[string]int{"go": 1, "python": 2, "C": 0}
	fmt.Println("m2=", m2)
}

/*
运行结果:
m1= map[0:go 1:python 2:rust]
m2= map[C:0 go:1 python:2]
*/
5.1.4 短变量初始化方式

如果 key 已存在,则会覆盖该 key 对应的 value

func main() {
	m1 := map[int]string{0: "C", 1: "go"} //初始化
	fmt.Println("m1=", m1)
	m1[0] = "rust"   //有该key则修改该key对应的value
	m1[3] = "python" //没有该key则追加,底层自动会扩容length
	fmt.Println("m1=", m1)
}

/*
运行结果:
m1= map[0:C 1:go]
m1= map[0:rust 1:go 3:python]
*/

5.2 map遍历

使用 for 循环搭配 range 进行遍历,第一个返回 key,第二个返回 value。遍历结果为无序,因为 map 本身就是无序的。
例:

func main() {
	m1 := map[int]string{0: "C", 1: "go", 2: "python", 3: "rust"} //初始化
	for key, value := range m1 {
		fmt.Printf("key=%d, value=%s\n", key, value)
	}
}

/*
运行结果:
key=2, value=python
key=3, value=rust
key=0, value=C
key=1, value=go
*/

5.3 判断key是否存在

语法:mapName[key]
返回 2 个结果:第一个是该 key 所对应的 value,第二个返回该 key 是否存在于 map 中的 bool。如果 key 不存在,value 返回其类型对应的零值。
示例:

func main() {
	m1 := map[int]string{0: "C", 1: "go", 2: "python", 3: "rust"} //初始化

	//第一个返回key对应的值,第二个返回key是否存在于map中的bool
	value, ok := m1[1]
	fmt.Printf("value=%v, ok=%v\n", value, ok)

	value, ok = m1[10] //不存在返回该值所对应的零值
	fmt.Printf("value=%v, ok=%v\n", value, ok)
}

/*
运行结果:
value=go, ok=true
value=, ok=false
*/

5.4 删除一对键值

语法:delete(mapName, key)
注:如果 keynil 或不存在与 map 中,delete 将会是个无操作指令(不会报错)!

func main() {
	m1 := map[int]string{0: "C", 1: "go", 2: "python", 3: "rust"} //初始化
	fmt.Println("m1=", m1)
	delete(m1, 3) //有这个key,就删除这对键值
	fmt.Println("m1=", m1)
	delete(m1, 10) //key不存在于map中,delete将是无操作指令
	fmt.Println("m1=", m1)
}

/*
运行结果:
m1= map[0:C 1:go 2:python 3:rust]
m1= map[0:C 1:go 2:python]
m1= map[0:C 1:go 2:python]
*/

5.5 map作为函数参数

map 作为函数参数传递是引用传递(传址),用的是同一个 map

import "fmt"

func test(m map[int]string) { //注意参数类型要一样
	delete(m, 0) //用的是同一个map,因此操作会影响到原有的map
	delete(m, 3)
}

func main() {
	m1 := map[int]string{0: "C", 1: "go", 2: "python", 3: "rust"} //初始化
	fmt.Println("m1=", m1)
	test(m1) //引用传递
	fmt.Println("m1=", m1)
}

/*
运行结果:
m1= map[0:C 1:go 2:python 3:rust]
m1= map[1:go 2:python]
*/



六、Struct 结构体

结构体是一种聚合的数据类型,它是由一系列具有相同类型或不同类型的数据构成的数据集合,每个数据称为结构体的成员。
简而言之:结构体可以将不同类型的数据整合成一个有机的整体。

6.1 结构体的初始化

基本语法:

type 变量名 struct {
	成员变量名1 数据类型
	成员变量名2 数据类型
	...
	成员变量名n 数据类型
}

注意:成员变量名前不要加var

6.1.1 普通变量

1.顺序初始化
顺序初始化,每个成员都必须给值,否则就报错

//定义一个结构体类型
type Student struct {
	id      int
	name    string
	sex     byte //字符类型,以ASCII码值打印
	age     int
	address string
}

func main() {
	//顺序初始化,每个成员都必须给值
	var s1 Student = Student{0, "go", 'M', 11, "Google Inc."}
	fmt.Println("struct s1=", s1)

	var s2 Student = Student{1, "python"} //报错:too few values in Student literal
	fmt.Println("struct s2=", s2)
}

2.指定成员初始化
指定成员初始化,没有指定的成员自动给其对应数据类型的零值

//定义一个结构体类型
type Student struct {
	id      int
	name    string
	sex     byte //字符类型,以ASCII码值打印
	age     int
	address string
}

func main() {
	//指定成员初始化
	s1 := Student{name: "python", address: "Netherlands"} //自动推导类型
	fmt.Println("s1=", s1) //s1= {0 python 0 0 Netherlands}
}
6.1.2 指针变量
//定义一个结构体类型
type Student struct {
	id      int
	name    string
	sex     byte //字符类型,以ASCII码值打印
	age     int
	address string
}

func main() {
	var s1 *Student //s1声明成一个Student结构体的指针
	s1 = &Student{0, "go", 'M', 11, "Google Inc."} //别忘了取地址符&
	fmt.Println("*s1=", *s1) //取值需要使用星花符*

	s2 := &Student{id: 1, name: "python", address: "Netherlands"} //自动推导类型为Student结构体指针
	fmt.Printf("s2 type is : %T\n*s2=%v", s2, *s2) //main包中,Student结构体指针类型
}

/*
运行结果:
*s1= {0 go 77 11 Google Inc.}
s2 type is : *main.Student
*s2={1 python 0 0 Netherlands}
*/

6.2 结构体成员的使用:普通变量

//定义一个结构体类型
type Student struct {
	id      int
	name    string
	sex     byte //字符类型,以ASCII码值打印
	age     int
	address string
}

func main() {
	//声明一个结构体普通变量
	var s1 Student

	//操作成员,使用点"."运算符
	s1.id = 0
	s1.name = "go"

	//未操作的成员,值为对应数据类型的零值

	fmt.Println("s1=", s1) //s1= {0 go 0 0 }
}

6.3 结构体成员的使用:指针变量

6.3.1 指针指向结构体普通变量的内存地址
//定义一个结构体类型
type Student struct {
	id      int
	name    string
	sex     byte //字符类型,以ASCII码值打印
	age     int
	address string
}

func main() {
	//指针有合法指向后,才能操作成员

	//1.定义一个普通结构体变量
	var s1 Student

	//2.定义一个结构体指针
	var p1 *Student

	//指针指向普通结构体变量的内存地址,指向一个合法的内存地址
	p1 = &s1

	//以下两种写法完全等价
	p1.id = 0
	(*p1).name = "go"
	p1.address = "Google Inc."

	fmt.Println("*p1=", *p1) //取指针中的值,需要使用星花符"*"
	fmt.Println("s1=", s1)   //直接打印s1的值
}

/*
运行结果:
*p1= {0 go 0 0 Google Inc.}
s1= {0 go 0 0 Google Inc.}
*/
6.3.2 new() 函数指向

new() 返回一个类型的指针,指针指向的是内存地址,相当于是去内存中申请了一块空间。
示例:

//定义一个结构体类型
type Student struct {
	id      int
	name    string
	sex     byte //字符类型,以ASCII码值打印
	age     int
	address string
}

func main() {
	//指针有合法指向后,才能操作成员

	//使用new()去申请一个结构体指针,留出一块内存空间并指向这块内存空间,也是一个合法的指向
	s1 := new(Student) //return *Type

	s1.name = "golang"
	(*s1).address = "Google Inc."
	s1.sex = 'M' //最终以ASCII码值的形式打印出来

	fmt.Printf("*s1=%v, s1 address: %p", *s1, &s1) //s1是一个合法指针,取值需要使用星花符"*",取地址使用&
}

/*
运行结果:
*s1={0 golang 77 0 Google Inc.}, s1 address: 0xc000006028
*/

6.4 结构体比较

结构体的比较只支持 ==!=

//定义一个结构体类型
type Student struct {
	id      int
	name    string
	sex     byte //字符类型,以ASCII码值打印
	age     int
	address string
}

func main() {
	s1 := Student{1, "go", 'M', 11, "Google Inc."}
	s2 := Student{1, "go", 'M', 11, "Google Inc."}
	s3 := Student{1, "go", 'M', 11, "Google Inc"} // 少了最后一个点"."

	fmt.Println("s1 == s2?", s1 == s2)
	fmt.Println("s1 == s3?", s1 == s3)
}

/*
运行结果:
s1 == s2? true
s1 == s3? false
*/

从以上示例中可以看出:结构体之间的比较是它们成员值的逐一完全比较,s3 的成员 address 字符串值中只少了一个 .,两者比较就是不相等了!

6.5 结构体赋值

同类型的两个结构体变量可以相互赋值。

//定义一个结构体类型
type Student struct {
	id      int
	name    string
	sex     byte //字符类型,以ASCII码值打印
	age     int
	address string
}

func main() {
	s1 := Student{1, "go", 'M', 11, "Google Inc."}

	var tmp Student //tmp也声明为Student类型
	tmp = s1 //只有同类型的结构体才能相互赋值,s1的类型也是Student
	fmt.Println("tmp=", tmp) //tmp= {1 go 77 11 Google Inc.}
}

6.6 结构体做函数参数:值传递

值传递:拷贝一份给形参,两个不同的作用域,修改的只是自己作用域内的。
示例:

//定义一个结构体类型
type Student struct {
	id      int
	name    string
	sex     byte //字符类型,以ASCII码值打印
	age     int
	address string
}

//值传递,函数内部无法修改外部的
func test(s Student) {
	s.id = 999
	fmt.Println("test s=", s)
}

func main() {
	s := Student{1, "go", 'M', 11, "Google Inc."}

	test(s) //s拷贝一份给形参

	fmt.Println("main s=", s)
}

/*
运行结果:
test s= {999 go 77 11 Google Inc.}
main s= {1 go 77 11 Google Inc.}
*/

6.7 结构体做函数参数:引用传递(传址)

拿到了一个变量的内存地址,使用的就是同一个变量了。

//定义一个结构体类型
type Student struct {
	id      int
	name    string
	sex     byte //字符类型,以ASCII码值打印
	age     int
	address string
}

//指针指向内存地址,拿到了内存地址,就是在使用同一个变量了
func test(s *Student) {
	s.id = 999 //此时s是指针了,指向了实参的内存地址,这里修改就会影响到实参
	(*s).address = "MountainView Google"
	fmt.Println("test s=", *s) //取出指针指向内存地址中的值,使用星花符"*"
}

func main() {
	s := Student{1, "go", 'M', 11, "Google Inc."}
	fmt.Println("before call test, main s=", s)

	test(&s) //s的地址传给形参

	fmt.Println("after called, main s=", s)
}

/*
运行结果:
before call test, main s= {1 go 77 11 Google Inc.}
test s= {999 go 77 11 MountainView Google}
after called, main s= {999 go 77 11 MountainView Google}
*/

6.8 带标签的结构体

结构体中的字段除了有名字和类型外,还可以有一个可选的标签 tag 。它是一个附属于字段的字符串,可以是文档或其他的重要标记,只有 reflect 包能获取它。
调用 reflect.TypeOf(variableName) (参数是一个变量名)可以获取该变量的正确类型。
如果变量是一个结构体类型,就可以通过 Field(int) 来索引结构体中的字段,然后就可以使用 Tag 属性。
示例:

import (
	"fmt"
	"reflect"
)

type TagType struct {
	field1 bool   "An important answer"
	field2 string "The name of the thing"
	field3 int    "How much there are"
}

func refTag(tt TagType, ix int) {
	ttType := reflect.TypeOf(tt) //获取到变量的类型
	ixField := ttType.Field(ix) //如果该变量的类型是结构体,那么才能去索引结构体中的字段
	fmt.Printf("%v\n", ixField.Tag)
}

func main() {
	tt := TagType{true, "Golang", 11}
	for i := 0; i < 3; i++ {
		refTag(tt, i)
	}
}

/*
运行结果:
An important answer
The name of the thing
How much there are
*/

6.9 声明内建类型的方法

不能在基本数据类型(例如:intstring)上创建方法。但可以利用一个技巧:创建一个基于内建数据类型的自定义数据类型,然后给这个自定义数据类型创建方法。

import (
	"fmt"
	"strings"
)

// 不可以为内建数据类型 string 创建方法
// 但可以基于 string,创建一个自定义数据类型,然后添加方法
type upperstring string

func (s upperstring) Upper() string {
	return strings.ToUpper(string(s))
}

func main() {
	s := upperstring("Learning Go!")
	fmt.Println(s.Upper())
}

/*
运行结果:

LEARNING GO!
*/



七、可见性

使某个符号对其他包可见(可以访问),需要大写该符号的首字母
如果想使用别的包的变量、函数、结构体类型、结构体成员,这些符号的首字母必须大写!
如果是小写,只能在同一个包里使用
注意:结构体变量名大写了,但里面的成员名没有大写,同样会导致其他包无法访问!
总结一句话:想要其他包访问到,首字母就大写!小写只能在同级包中可见!


八、关于 nil 的笔记

nil 是 Golang 中的预定义标识符。用来表示:mapslicechannelfunctioninterfacepointer 的零值。

8.1 未显示声明的数据类型,不能使用 nil

错误示例:

func main() {
	var x = nil // use of untyped nil
	_ = x
}

这段代码在编译时就会错误:use of untyped nil。大意:使用无类型的 nil
nil 用来表示 6 个引用类型的零值,有 6 个引用类型,编译器无法猜测这个 nil 到底表示哪一个数据类型。
所以,必须要显示声明具体的数据类型后,才能使用 nil
正确示例:

func main() {
	// slice
	var sli []int
	sli = nil
	_ = sli

	// map
	var m map[string]int
	m = nil
	_ = m

	// pointer
	var p *int // 指向 int 类型的指针
	p = nil
	_ = p

	// channel
	var ch chan string // 一个传递 string 类型数据的通道
	ch = nil
	_ = ch

	// function
	var f func(a, b int) int // 声明一个函数,接收两个 int 类型的参数,并最终返回一个 int 类型的值
	f = nil
	_ = f

	// interface
	var x interface{}
	x = nil
	_ = x
}

能够通过编译,一点问题都没有。

8.2 Slicemapnil 时,各自的区别

8.2.1 当 Slicenil

当一个切片为 nil 时,往切片中添加元素是没有任何问题的。
示例:

func main() {
	var s []int
	s = nil // 手动赋值了 nil,也不会影响接下来的元素添加操作
	s = append(s, 111)
	s = append(s, 222)
	fmt.Println("s =", s)
}

/*
运行结果:
s = [111 222]
*/

个人分析原因:切片是对底层数组的引用,当有元素往切片中添加时,首先会检查该切片的容量。如果容量不够,就会先去扩容底层数组,然后再对该底层数组进行引用。设置为 nil 只是表示不去引用底层数组,当有元素往切片中添加,那么切片就需要一个底层数组了,Go 内部会去自动引用一个底层数组。

8.2.1 当 mapnil

Slicenil 时,如果往 map 中添加元素,就会导致运行时的 panic
错误示例:

func main() {
	var m map[string]string
	m["a"] = "aaa" // panic: assignment to entry in nil map
	fmt.Println("m =", m)
}



九、小案例

例1,生成随机数

import (
	"fmt"
	"math/rand"
	"time"
)

func main() {
	//1.设置种子
	rand.Seed(time.Now().UnixNano()) //种子参数写死的话,每次随机数都一样了。所以把当前时间转换成纳秒作为种子参数

	//2.生成随机数
	for i := 0; i < 5; i++ {
		fmt.Println("rand a number:", rand.Intn(100)) //限定100以内的整数
	}
}

/*
运行结果:
rand a number: 31
rand a number: 24
rand a number: 58
rand a number: 86
rand a number: 28
*/

例2,猜数字小游戏

import (
	"fmt"
	"math/rand"
	"time"
)

//随机生成一个4位数
func InitSlice(s []int) {
	n := cap(s)
	rand.Seed(time.Now().UnixNano())

	for i := 0; i < n; i++ {

		//第1位数字不能为0
		if i == 0 {
			for {
				r := rand.Intn(10)
				if r == 0 {
					continue
				} else {
					s[0] = r
					break
				}
			}
		}

		s[i] = rand.Intn(10)
	}
}

//获取输入的4位数的每一位数字
func GetEveryNum(guessSlice []int, guessNum int) {
	guessSlice[0] = guessNum / 1000
	guessSlice[1] = guessNum / 100 % 10
	guessSlice[2] = guessNum / 10 % 10
	guessSlice[3] = guessNum % 10
}

func Guess(s []int) {
	n := cap(s)
	guessSlice := make([]int, 4)

	for {
		var guessNum int

		//校验输入的数字是否是一个4位正整数
		for {
			if guessNum < 1000 || guessNum > 9999 {
				fmt.Printf("enter a guess number between 1000 and 9999:")
				fmt.Scan(&guessNum)
			} else {
				break
			}
		}

		GetEveryNum(guessSlice, guessNum)

		right := 0
		for i := 0; i < n; i++ {
			if guessSlice[i] < s[i] {
				fmt.Printf("position %d number is small\n", i+1)
			} else if guessSlice[i] > s[i] {
				fmt.Printf("posistion %d number is big\n", i+1)
			} else {
				fmt.Printf("position %d number is right\n", i+1)
				right++ //猜对1位就自加1
			}
		}

		//4位全部猜对打印信息并退出循环
		if right == n {
			fmt.Println("success.")
			break
		}
	}
}

func main() {
	s := make([]int, 4, 4)
	InitSlice(s)

	Guess(s)
	fmt.Println("init s, s=", s)
}

/*
运行结果:enter a guess number between 1000 and 9999:1234
position 1 number is small
position 2 number is small
position 3 number is small
posistion 4 number is big
enter a guess number between 1000 and 9999:5555
position 1 number is small
position 2 number is right
position 3 number is right
posistion 4 number is big
enter a guess number between 1000 and 9999:6553
position 1 number is right
position 2 number is right
position 3 number is right
posistion 4 number is big
enter a guess number between 1000 and 9999:6551
position 1 number is right
position 2 number is right
position 3 number is right
posistion 4 number is big
enter a guess number between 1000 and 9999:6550
position 1 number is right
position 2 number is right
position 3 number is right
position 4 number is right
success.
init s, s= [6 5 5 0]
*/
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值