目录
1. go 数组
1.1 go的数组申明方式,初始化方式,以及赋值方式
代码如下:
var arr1 [5]int // 数组的申明
arr1[0] = 0 //数组的赋值
arr1[1] = 1
// ...
arr1[4] = 4
fmt.Println("arr1:", arr1)
var arr2 = [5]int{0, 1, 2, 3, 4} //申明加初始化
fmt.Println("arr2:", arr2)
arr3 := [5]int{0: 5, 1: 0} //字面量声明加部分初始化 [index:value]
fmt.Println("arr3:", arr3)
//如果数据长度不确定我们可以使用[...] ,系统会自动推断数组长度
//这个有点类似C中 int a[] = {1,2,3},系统会自动知道这个a 会有3个元素,无非就是go中你要给他写上[...]
arr4 := [...]int{10, 9, 8, 7, 6, 5, 4, 3, 2, 1}
fmt.Println("arr4:", arr4)
运行结果如下:
arr1: [0 1 0 0 4]
arr2: [0 1 2 3 4]
arr3: [5 0 0 0 0]
arr4: [10 9 8 7 6 5 4 3 2 1]
1.2 数组在函数间传值
go数组和C数组的传参上区别还是很大,虽然go中有指针(后面会讲),但是数组名作为参数时,并不会想C一样退化成指针,所以函数形参的一定要和数组大小一样才能传的,比如函数形参写的是tmp []int,要传的数组是arr := [5]int,这样子传arr是不行的,最常见的做法应该是把数据伪装成切片进行传参(sum(arr5[:])),要么就是把函数形参改成tmp [5]int,要么就把数组定义成切片(切片后面会说)
下面的代码展示数组在函数间传参的方式
/*
arr5 := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
sum(arr5) 这是一个值传递
change(&arr5) 这是一个引用传递
传入的数组的申明定义
*/
func sum(para [10]int) int {
fmt.Printf("len:%d\nsize:%d \n%v\n", len(para), unsafe.Sizeof(para), para)
var Sum int
for _, val := range para {
Sum += val
}
para[0] = 100
fmt.Println(Sum)
return Sum
}
func change(para *[10]int) {
for index, val := range para {
para[index] = val * 10
fmt.Print(val, " ")
}
}
运行结果如下:
arr1: [0 1 0 0 4]
arr2: [0 1 2 3 4]
arr3: [5 0 0 0 0]
arr4: [10 9 8 7 6 5 4 3 2 1]
len:10
size:80
[1 2 3 4 5 6 7 8 9 10]
55
----
before change :[1 2 3 4 5 6 7 8 9 10]
after change :[10 20 30 40 50 60 70 80 90 100]
数据的值传递没啥好说的,跟其他语言大差不差。下面的change函数是go中的引用传递(不知道有没有理解错),这个跟C的指针传递写法与C++的引用传递写法还是要注意一下,写法上像C传指针,使用上像C++的引用。(这块可能描述的不是很好,大家可以评论区讨论一下)
2. go 切片
go的切片是抽象数组的一个概念,很容易混淆。简单说就是数组的长度是固定的,切片是可以变化长度的数组。
2.1切片的申明定义
数组在上一节讲过了,那切片和数组在申明上有哪些区别呢? 在用var申明定义时,其实切片和数组就是一个有没有指定长度的区别 ,数组在定义式它的地址是不为空的,切片在这里的地址是为nil(0x0)。同样也可以使用短变量定义 ,如s2,内置函数make可以初始化切片,这里会初始化一个长度为5,容量为10的切片,并且前五个元素会被初始化为0.
代码如下:
var arr1 = [5]int{} //定义一个数组,但未初始化
var s1 []int //定义一个切片,但未初始化
fmt.Printf("数组的地址:%p,切片的地址:%p\n", &arr1, s1)
s1 = make([]int, 5, 10)
s2 := make([]int, 5, 10) //等价s1
fmt.Printf("s1切片的地址:%p,s2切片的地址:%p\n", s1, s2)
运行结果如下:
数组的地址:0xc0001a4000,切片的地址:0x0
s1切片的地址:0xc0001ae000,s2切片的地址:0xc0001ae050
2.2 切片的增删改查
这里使用引用数组的切片,使用make定义的也行。切片本质也是数组,所以同样可以使用下标进行修改和查看内容,删除也和数组差不多(删除的方法很多这里是随便写一个)。还有一个就是增加,go中有一个内置函数append,可以用来增加切片的内容。如果切片的长度小于容量则只增加长度,并填入内容;如果长度等于容量,它会自动扩展容量(倍增)。
代码如下:
//切片引用数组
arr1 := [...]int{1, 2, 3, 4, 5, 6}
s1 := arr1[:]
s1[0] = 100 // 修改
//s1[6] = 1 错误,因为s1的容量只有6
fmt.Println("cap:", cap(s1))
s1 = append(s1, 99) //增加切片内容
fmt.Println("cap:", cap(s1)) //当切片的长度和容量一样大的时候,使用append会是切片容量翻倍
//删除切片的某个元素,假设我要删除s1[2]的元素
index := 2
for i, _ := range s1 {
if index <= i && i != len(s1)-1 {
s1[i] = s1[i+1]
i++
}
}
s1 = s1[:len(s1)-1]
//s1 = append(s1[:index], s1[index+1:]...) 这样写也是可以的,但是如果s1是形参,这是改变不了s1本身的
fmt.Println(s1)
运行代码如下:
cap: 6
cap: 12
[100 2 4 5 6 99]
2.3 切片传参的使用
切片属于引用传递,所以函数可以直接使用切片的形参修改切片本身
/*
s3 := make([]int, 1)
sli((s3))
fmt.Println(s3)
*/
func sli(s []int) []int {
fmt.Println("len:", len(s), "cap:", cap(s), "s:", s)
s[0] = 1
return s
}
运行结果如下:
len: 1 cap: 1 s: [0]
[1]
切片的内容就讲到这边,但是切片的内容远远不止这些,比如操作切片的内置函数有哪些?(append,copy,len,cap,make)这里面有许多的知识点和注意点,就不在这里说了。
3. go Map
Map 是一个键值对的集合,并且包含键值对的索引。像是一个数组中每个元素是一对键值对,这对键值对在数组中有特定的索引。但是Map是一个无序的,意味着我们每次访问前,都不知道每个键值对的索引。简单说Ma就是一个无序的键值对集合。
3.1 申明定义一个Map
下面演示了两种定义初始化方式,一种make(map[key-type]value-type,capcity),一种是短变量初始化。这个用make 定义的时候虽然第二个参数是cap参数,但是内置函数cap是没法获得一个Map的容量大小的,可以使用len()获得键值对个数。
m1 := make(map[string]int, 5) 创建一个初始容量为 5 的 Map
m1["age"] = 1 //对Map进行赋值
m1["class"] = 2
m1["hello"] = 3
m1["world"] = 4
m1["welcome"] = 5
fmt.Printf("&m1:%p, m1_sz:%d,len:%d , m1:%v\n", m1, unsafe.Sizeof(m1), len(m1), m1)
m1["add"] = 6
m1["del"] = 7
fmt.Printf("&m1:%p, m1_sz:%d,len:%d , m1:%v\n", m1, unsafe.Sizeof(m1), len(m1), m1)
m2 := map[string]int{
"age": 11,
"class": 1, //最后一对键值对的逗号不能去掉!
}
fmt.Printf("len:%d , m1:%v\n", len(m2), m2)
运行代码如下:
&m1:0xc000094150, m1_sz:8,len:5 , m1:map[age:1 class:2 hello:3 welcome:5 world:4]
&m1:0xc000094150, m1_sz:8,len:7 , m1:map[add:6 age:1 class:2 del:7 hello:3 welcome:5 world:4]
len:2 , m1:map[age:11 class:1]
3.2 Map的查看和遍历
下面代码展示了Map的遍历和查看,这里提一下这句 val, t := m1["e"],val指的是m1["e"]所对应的value,如果没有这个key,那么val是这个类型的初始值;t是bool类型,存在key,t就为true,反之为false.
m1 := map[string]string{
"h": "w",
"e": "o",
"l": "r",
"o": "l",
}
fmt.Println(m1) //可以直接print
for val, t := range m1 { //也可以通过range
fmt.Println(val, ":", t)
}
fmt.Println("m1[\"h\"]", m1["h"]) //也可以通过key 作为索引 查看对应的value
val, t := m1["e"] // 查找存在的key
fmt.Println(val, t)
val, t = m1["z"] // 查找不存在的key
fmt.Println(val, t)
代码运行如下,通过打印m1也可以看出Map的无序性
map[e:o h:w l:r o:l]
l : r
o : l
h : w
e : o
m1["h"] w
o true
false
3.3 Map的传参以及增删改查
Map也是属于引用传递
/*
m1 := map[string]int{
"age": 11,
"class": 1,
"grade": 1,
}
fmt.Println("befor modify:", m1)
Map_crud(m1)
fmt.Println("after modify:", m1)
*/
func Map_crud(m map[string]int) map[string]int {
fmt.Println(m)
m["age"] = 2023 // 修改
m["goal"] = 150 // 增
delete(m, "class")
return m
}
运行结果如下:
befor modify: map[age:11 class:1 grade:1]
map[age:11 class:1 grade:1]
after modify: map[age:2023 goal:150 grade:1]
4. go 指针
4.1 go指针定义
go的指针比C的指针限定的条件比较多,没有C那样可以随意操作指针。
需要注意几点:
1. go的数组名不是首地址,所以不能当做指针使用
2. go的指针不支持下标检索
3. go的指针是不支持运算的,例如p++,p--之类(但是有办法可以运算,下面会说)
var a int = 5
var p *int = &a
fmt.Printf("&a:%p,p:%p\n", &a, p) // a 的地址和 p指向的地址一样
fmt.Printf("a:%d,*p:%d\n", a, *p) //a = *p = 5
a1 := [...]int{1, 2, 3, 4, 5}
p = &a1[0] //Go中的数组是不会退化成指针的
fmt.Printf("*p : %d\n", *p)
运行结果如下:
&a:0xc000122000,p:0xc000122000
a:5,*p:5
*p : 1
4.2 go指针的运算
上面有提到go是不支持指针运算的,所以我们是没办法直接对指针进行运行,需借助unsafe包,写到这边我也知道为什么叫做unsafe(不让这样做,但是可以做,所以不安全嘛)。
对go的指针运算我们用到了unsafe.Pointer()和uintptr.通过这两个内置我们可以把指针转换为无符号整形数进行计算,最后再把这个整形数在转换为指针。
代码如下(如没有特殊需求,建议安装上方注释的方法写。个人理解:Go团队似乎很不建议开发者对指针进行运算,所以中间过程尽量不要用变量接收,防止误改,而且这里没注释的代码,也有一个警告:possible misuse of unsafe.pointerunsafeptr):
/*
a2 := [...]int{9, 8, 7, 6, 5, 4, 3, 2, 1, 0}
p1 := &a2[0]
tmp := ((*int)(unsafe.Pointer((uintptr(unsafe.Pointer(p1))) + 16))) //tmp 的地址等于 &a2[2]
fmt.Println(*tmp, tmp, &a2[2])
*/
a2 := [...]int{9, 8, 7, 6, 5, 4, 3, 2, 1, 0}
p1 := unsafe.Pointer(&a2[0])
uint_p1 := uintptr(p1)
uint_p1 += 16
p2 := (*int)(unsafe.Pointer(uint_p1))
fmt.Println(*p2) //输出 7 ,与a2[2]相等
4.3 go指针的传参
这里ptr_test()函数 中的a 讲道理是局部变量,p1是一个指针指向a,但是return出p1,外部依然可以访问p1这个指针的内容,这个应该是go的内存机制有关,似乎是变量逃逸的知识点。写到这里的时候我还没怎么了解变量逃逸,后面单独出一个章节讲讲变量逃逸吧
代码如下:
/*
main
a2 := [...]int{9, 8, 7, 6, 5, 4, 3, 2, 1, 0}
p2 := &a2[0]
p3 := ptr_test(p2)
fmt.Println(*p3)
fmt.Println(a2)
*/
func ptr_test(p *int) *int {
fmt.Println(*p) // 9
*p = 99 //修改
a := 5
p1 := &a
return p1 //这里与C语言不一样,需注意
}
运行结果如下:
9
5
[99 8 7 6 5 4 3 2 1 0]
4.4 go指针进阶
指针数组 (点击直接跳转)
多级指针(这里只讲二级指针):还没写
函数指针 :还没写
(这部分后期打算单独写,以连接的形式跳转)
5. go 结构体
5.1 go结构体的申明定义初始化
type是Go中的类型定义关键字,这里定义一个结构体类型 ,写法 type [NewTypeName] struct
type Class struct {
num int
grade int
name string
}
func main() {
c1 := Class{1, 1, "hello"} //直接初始化
c2 := Class{num: 2, grade: 2, name: "world"} //通过键值对初始化
fmt.Println(c1, "\n", c2)
}
5.2 go结构体传参使用
go结构体属于普通类型,传参时需注意值传递,地址传递的区别
/*
c1 := Class{1, 1, "hello"}
struct_test(c1)
fmt.Println(c1)
p_struct_test(&c1)
fmt.Println(c1)
*/
// 值传递
func struct_test(c Class) {
c.name = "new Name"
c.num = 99
c.grade = 888
}
// 地址传递
func p_struct_test(c *Class) {
c.name = "new Name"
c.num = 99
c.grade = 888
}
运行结果如下:
{1 1 hello}
{1 1 hello}
{99 888 new Name}
总结:
码到现在算是差不多了,如果存在说法错误或者不严谨,大家可以留言评论。因为我自己是只会C,其他啥也不会。所以文中多出拿C与Go的概念作比较,有些也不知道合不合适。有一些遗留问题,后面会在评论区里解答。或者大家帮我哈。互相学习进步!!!
遗留问题
1. slice 的用Sizeof 计算为什么是24?16不也直接对齐吗?
2. 同上Sizeof(map)为什么是8,map不是引用类型吗?
3. make 和 new 是动态分配内存,但似乎Go的垃圾回收机制会自动回收,那这个垃圾回收的原理有是什么?
4. 地址问题,为啥地址一开始加8个字节,中间变成加2个字节,然后又8个字节?
a2 := [...]int{9, 8, 7, 6, 5, 4, 3, 2, 1, 0}
fmt.Println(&a2[0], &a2[1], &a2[2], &a2[3]) // 0xc000026050 0xc000026058 0xc000026060 0xc000026068??
5. 怎么去除掉这个警告possible misuse of unsafe.pointerunsafeptr?
p2 := (*int)(unsafe.Pointer(uint_p1)) //(unsafe.Pointer(uint_p1))这里似乎有一个警告possible misuse of unsafe.Pointerunsafeptr