前言
这是我学习Go语法的笔记。由于有C和Python的基础,上手Go很快。笔记很粗糙,好在自己够用。
此篇包括了Go相关的:指针,数组,切片,字典,结构体等。涉及关键字struct
。
指针
Go语言中的指针与C稍异。如C中,声明一个int类型的指针语句:int* p
,所以p的类型就为int*
。而Go语言中,*
在前面:
func main() {
num := 512
var p *int // Go语言中‘*’在前面
p = &num
fmt.Printf("address of num is %p", p)
}
由于Go语言在声明变量的时候会自动初始化值,对于指针,会指向nil,而不是NULL(Go中没有NULL):
func main() {
var p *int
fmt.Printf("address of pointer is %p, and its content is %v", p, p)
}
// 输出:
address of num is 0x0, and its content is <nil>
Go语言不允许操作没有合法指向的内存:
func main() {
var p *int
*p = 512 // 对p指针指向的内存赋值
fmt.Printf("address of pointer is %p, and its content is %v", p, p)
}
// 抛异常:
panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xc0000005 code=0x1 addr=0x0 pc=0x490056]
new
new()函数是用来分配内存的内建函数,用法为new(T)
(T是类型),会将分配的内存置为0值,同时返回该内存的地址:
func main() {
p := new(int) // 返回*int指针
fmt.Printf("type of pointer is %T\n", p)
fmt.Printf("content of memory is %v\n", *p)
}
// 输出:
type of pointer is *int
content of memory is 0
此时你可以正确操作指针p指向的内存,比如正确的赋值:
*p = 512 // 不可以赋值字符串
从C语言角度理解这个过程,可以近似为:
int main() {
int *p = (int*)malloc(sizeof(int)); // 申请内存
memset(p, 0, sizeof(int)); // 内存置0操作
printf("address of memory is %p, content of memory is %d", *p, *p);
free(p); // 释放操作
return 0;
}
重点在于,Go语言有完善的GC机制,不需要手动释放内存;而C中,每一个malloc后,都应该有对应的free
操作。
数组
Go中的数组声明与C也不同:
// Go
var array [3]int // 声明一维数组
var array [2][3]int // 声明二维数组
var array = [3]int{1, 2, 3} // 对一维数组赋值
array := [3]int{1, 2, 3} // 简短声明一维数组
var array = [2][3] // 对二维数组赋值
array := [2][3]int{{1, 2, 3}, {3, 2, 1}} // 简短声明一维数组
var arr = [...]int{1, 2, 3, 4} // 允许使用`...`自动计算长度
arr := [...]int{1, 2, 3, 4}
// C
int array[3]; // 声明一维数组
int array[2][3]; // 声明二维数组
int array[3] = {1, 2, 3};
int array[2][3] = {{1, 2, 3}, {3, 2, 1}};
对Go中的数组,有以下几点需要注意:
数组声明时不可以用变量表示元素个数,如:
// 错误操作 n := 10 array := [n]int
数组可以指定元素初始化(未指定元素默认0值),如:
func main() { array := [5]int{2: 512, 4: 1024} // 冒号前边的数表示索引值 for i := range array { fmt.Printf("%d ", array[i]) } }
0 512 0 1024
数组支持
==
或!=
运算,前提是数组类型要一样,即[x][y]type
(eg:2int) 一致:func main() { arr1 := [2][3] int {{1, 2, 3}, {3, 2, 1}} arr2 := [2][3] int {{1, 2, 3}, {3, 2, 1}} fmt.Println("arr 1 == arr2 ?", arr1 == arr2) arr3 := [2][3] int {{2, 1, 3}, {3, 2, 1}} arr4 := [2][3] int {{1, 2, 3}, {3, 2, 1}} fmt.Println("arr 3 == arr4 ?", arr3 == arr4) } // 输出 arr 1 == arr2 ? true arr 3 == arr4 ? false
同类型的数组允许相互赋值:
var arr1 = [2]int {1, 2} var arr2 [2]int arr2 = arr1
Go语言中,传递数组是值传递:
func change(arr [2]int) {
arr[0] = 512
}
func main() {
arr := [2]int{}
fmt.Println("before call change(): ", arr[0])
change(arr)
fmt.Println("after call change(): ", arr[0])
}
// 输出:
before call change(): 0
after call change(): 0 // 值未被改变
然而C语言中,传递数组是指针传递:
void change(int arr[])
{
arr[0] = 512;
}
int main() {
int arr[2] = {0};
printf("before call change(): %d\n", arr[0]);
change(arr);
printf("after call change(): %d\n", arr[0]);
}
// 输出:
before call change(): 0
after call change(): 512 // 值被改变
切片
切片与数组的区别
区别1:声明数组时,中括号写明了数组的长度或使用...
自动计算长度,声明slice时,中括号内没有任何字符,如:var s = []int
或者s := []int{}
。
区别2:array静态数组拥有固定的长度,达到容量限制之后就无法再添加元素;Go语言中的切片与Python的list概念很相似,可以通过扩容的方式突破容量限制(也可以认为因为扩容而使得容量值一直在改变)。
区别3:作为函数的参数传递时,数组是值传递,切片为引用传递。
创建切片的方式
var方式
var s = []int
简短声明
s := []int{}
make方式
s := make([]int, 3, 5)
fmt.Printf("切片的长度:%d,切片的容量:%d\n", len(s), cap(s))
// 输出:
切片的长度:3,切片的容量:5
make的第一个参数表示切片类型,第二个参数表示切片的长度,第三个参数表示切片的容量。
第四种:对一个现有数组切片
var arr = [5]int{1, 2, 3, 4, 5}
s := arr[1:3:5]
切片的分析
切片由三部分组成:指针,长度,容量。指针指向slice对应的底层数组(中的某个元素),长度表示slice中一共存放了多少个元素,容量表示slice最多可以存放多少个元素。当slice中的元素数量超过容量时,会重新开辟一个大于当前元素数量的空间来存放现有元素。
不妨先从元素个数大于容量开始说起:
func main() {
var s []int // 声明一个切片
fmt.Printf("切片的首地址:%p,切片的长度:%d,切片的容量:%d\n", s, len(s), cap(s))
s = append(s, 1, 2, 3) // 向切片里追加1,2,3
fmt.Printf("切片的首地址:%p,切片的长度:%d,切片的容量:%d\n", s, len(s), cap(s))
s = append(s, 4)
fmt.Printf("切片的首地址:%p,切片的长度:%d,切片的容量:%d\n", s, len(s), cap(s))
s = append(s, 5)
fmt.Printf("切片的首地址:%p,切片的长度:%d,切片的容量:%d\n", s, len(s), cap(s))
}
// 输出
切片的首地址:0x0,切片的长度:0,切片的容量:0
切片的首地址:0xc0000580c0,切片的长度:3,切片的容量:4
切片的首地址:0xc0000580c0,切片的长度:4,切片的容量:4
切片的首地址:0xc000088100,切片的长度:5,切片的容量:8
可以看到,当往切片里存放三个元素后(1,2,3),切片s的长度为3,容量为4;然后添加一个元素(4),s长度为4,容量为4,此时前后两次,s的地址是相同的。再往切片追加一个元素(5),超过了切片原有容量,于是s重新找一块地“占山为王”,新的“山头”可以容纳更多人,因此你看到了最后一次输出结果:s有了新的地址,也有了新的容量值。
以上方式创建的切片会让我们察觉不到底层数组的存在,因此我们可以换一种方式创建切片来感受它与数组间的关联:
arr := [5]int{1, 2, 3, 4, 5}
s := arr[1:3:4] // 重点看这一句
如果你会Python,你可能感到熟悉,但这并非我们熟悉的Python,这是Go语言。其中1表示从数组arr的下标为1的元素起(也就是说,切片的指针指向该数组的第二个元素);3表示对数组arr的坐标取值到3-1的位置(这一点倒与Python的切片一样);最后一个4表示设置切片的容量,容量为:4-1。
因此这里有一个格式:s := arr[low:high:max]
。切片s长度为:high - low,容量为:max - low。而max需要小于等于数组arr的长度。
func main() {
arr := [5]int{1, 2, 3, 4, 5}
s := arr[1: 3: 4]
fmt.Printf("数组第2个元素的地址:%p\n", &arr[1])
fmt.Printf("切片的首地址:%p,切片的长度:%d,切片的容量:%d\n", s, len(s), cap(s))
fmt.Printf("切片的内容:%v", s)
}
// 输出:
数组第2个元素的地址:0xc00006e068
切片的首地址:0xc00006e068,切片的长度:2,切片的容量:3
切片的内容:[2 3]
切片作为底层数组引用的存在,能够影响数组的值。现在我们的切片s就引用了数组arr,改动s就是在改动arr:
fmt.Println("数组:", arr)
fmt.Println("切片:", s)
s[0] = 512 // 改变切片
fmt.Println("数组:", arr)
fmt.Println("切片:", s)
// 输出:
数组: [1 2 3 4 5]
切片: [2 3]
数组: [1 512 3 4 5]
切片: [512 3]
需要记住的是,只有在s还是arr的引用时才有这样的作用。比方如果往s添加元素以致超过了切片原有容量,切片会重新划分空间,s就不再是arr的引用,不会再影响arr:
func main() {
arr := [5]int{1, 2, 3, 4, 5}
s := arr[1:3:4]
fmt.Printf("数组的内容:%v\n", arr)
fmt.Printf("切片的内容:%v\n", s)
fmt.Printf("切片的首地址:%p,切片的容量:%d\n", s, cap(s))
s = append(s, 10,) // 切片追加1个元素,此时还未超过切片的容量
fmt.Println()
fmt.Printf("数组的内容:%v\n", arr)
fmt.Printf("切片的内容:%v\n", s)
fmt.Printf("切片的首地址:%p,切片的容量:%d\n", s, cap(s))
s = append(s, 11,) // 再追加1个元素,超过切片原有容量
fmt.Println()
fmt.Printf("数组的内容:%v\n", arr)
fmt.Printf("切片的内容:%v\n", s)
fmt.Printf("切片的首地址:%p,切片的容量:%d\n", s, cap(s))
}
// 输出:
数组的内容:[1 2 3 4 5]
切片的内容:[2 3]
切片的首地址:0xc00000c368,切片的容量:3
数组的内容:[1 2 3 10 5] // 数组值被影响
切片的内容:[2 3 10]
切片的首地址:0xc00000c368,切片的容量:3
数组的内容:[1 2 3 10 5] // 数组值未被影响
切片的内容:[2 3 10 11]
切片的首地址:0xc00000c3f0,切片的容量:6
切片的用法
操作 | 含义 |
---|---|
s[n] | 切片 s 中索引位置为 n 的元素 |
s[:] | 从切片 s 的索引位置 0 到 len(s)-1 处获得的切片 |
s[low:] | 从切片 s 的索引位置 low 到 len(s)-1 处获得的切片 |
s[:high] | 从切片 s 的索引位置 0 到 high 处获得的切片, len=high |
s[low:high] | 从切片 s 的索引位置 low 到 high 处获得的切片,len=high-low |
s[low:high:max] | 从切片 s 的索引位置 low 到 high 处获得的切片,len = high-low, cap=max-low |
len(s) | 切片 s 的长度,总是<=cap(s) |
cap(s)] | 切片 s 的长度,总是>=len(s) |
切片的两个方法
append:追加效果
s = append(s, x, y, z, ...) // 切片 s 中追加x, y, z 等元素
s = append(s, n...) // 切片 s 中追加切片 n 中的元素
copy:实现覆盖效果
x_slice := []int{1, 2, 3}
y_slice := []int{10, 20, 30, 40}
copy(y_slice, x_slice)
fmt.Println(y_slice)
// 输出:
[1 2 3 40]
x_slice中三个元素覆盖掉了y_slice中的前三个元素。
切片的其他说明
- 切片仅允许与nil使用
==
、!=
,即便是类型相同的两个切片之间也不能使用比较运算符; - 同类型的切片之间允许相互赋值(
s1
)。
map
键值对数据类型,与Python中的字典类型(dict_ = {}
)相似。
创建map的方式
var方式
var m map[int]string // 无自定义初始化值
var m = map[int]string{1:"bobby"} // 有自定义初始化值
简短声明
m := map[int]string{}
m := map[int]string{1: "boby"}
make方式
m := make(map[int]string)
m := make(map[int]string, 2) // 指定容量大小
中括号([]
)内为键类型,中括号的右边为值类型。
需要注意以下几点:
不允许向一个nil值的map存入元素,会引发panic;
var m map[int]string fmt.Println(m == nil) m[1] = "bobby" fmt.Println(m) // 输出: true panic: assignment to entry in nil map
解决方法,用以下方式创建map:
var m = map[int]string{} m := map[int]string{} m := make(map[int]string)
- 当有
m[1] = "boby"
语句时,如果m中没有关键字1,就会动态创建一个关键字为1值为"boby"的键值。如果没有指定map容量,map可能会频繁扩容来满足添加元素的需求,效率会降低,所以建议用m := make(map[int]string, capValue)
方式创建字典,指明对map的预期容量(尽管如此,却不能对map类型使用cap()
方法); - map类型的键值应该是确定并且唯一。确定:不应该用切片、函数等作为键值;唯一:同一个map中,不会有相同的键值;
- map类型的变量作为参数实参传递时,用引用传递方式;
- map类型的变量内部是无序的(与Python字典无序一样)。
判断键值是否存在
func main() {
m := map[int]string{
0: "bobby",
1: "nike",
}
value, ok := m[1] // 重点
if ok {
fmt.Printf("m[1]的值为:%v\n", value)
fmt.Printf("ok 的值为:%v\n", ok)
} else {
fmt.Printf("不存在m[1]")
}
}
如果键值1存在,则ok为true,否则false。
删除键值对
delete(m, 1)
第一个参数为map类型的变量,第二个参数为键值。无论删除的键值是否存在m中,都不会引发panic。
map的遍历
for key, value := range m{
// ...
}
结构体
定义一个结构体
结构体的声明方式:
type StructName struct {
// ...
}
此时StructName是一种数据类型。放一个完整示例:
func main() {
type student struct {
name string
age int
id uint64
} // 定义一个名为 student 的结构体
var s student // 声明一个 student 类型的变量 s
s.name = "zty"
s.age = 22
s.id = 201431060011
fmt.Println(s)
}
// 输出:
{zty 22 201431060010}
通过.
操作访问成员变量。这与C语言中的结构体仅仅存在细微差异:
int main()
{
typedef struct {
char* name;
int age;
unsigned long long id;
} student;
student s;
s.name = "zty";
s.age = 22;
s.id = 201431060010;
printf("%s %d %lld", s.name, s.age, s.id);
}
// 输出:
zty 22 201431060010
结构体变量的声明
var s = student{"zty", 22, 201431060010}
s := student{"zty", 22, 201431060010}
s := new(student) // 注意,此时s是指针类型
与C中结构体存在最大的差别是:Go语言中,指向结构体的指针访问成员时使用.
而不是->
C语言如下:
student stu; // stu 是一个结构体
student* s = &stu; // s 是指向 stu 结构体的指针
s->name = "zty"; // `->` 的访问方式
s->age = 22;
s->id = 201531060010;
而Go语言:
var stu student
s := &stu
s.name = "zty"
s.age = 22
s.id = 201431060010
fmt.Println(s)
// 输出:
&{zty 22 201431060010} // 注意这个取地址符
关于结构体变量初始化值也是有讲究的。要求顺序初始化必须对每个成员初始化:
s := student{"zty", 22, 20140010} // 正确
s := student{"zty"} // 错误
而指定成员初始化中,允许不对每个成员初始化(未初始化的成员默认0值):
s := student{age:22}
fmt.Println(s)
// 输出:
{ 22 0} // 这里面有三个值,第一个空字符串(name),第二个22(age),第三个0(id)
结构体的其他说明
- 允许结构体参与
==
和!=
运算,但不能>
或<
; - 同类型的结构体变量可以指相互赋值(
s1 = s2
); - 结构体作为函数实参传递时,使用值传递方式;
- 结构体需要对外部可见时,无论是类型名还是成员名都必须首字母大写(
type Student struct { Name string}
)。
随机数
Go中获取随机数分两步骤:
- 设置随机种子;
获取随机数。
import ( "fmt" "math/rand" "time" ) func main() { rand.Seed(time.Now().UnixNano()) // 设置随机种子 // time.Now().UnixNano() 表示系统时间,因为时间总是在变化,借此得到不同的随机种子 for i:=0; i < 3; i++ { fmt.Println(rand.Intn(100)) } }
rand.Intn(n)
表示在0~n范围内获取随机数。
还不快抢沙发