Go语言基础学习(三)

指针

Go 语言中和 C/C++ 一样,直接提供了指针类型,但是不同的是 Go 语言的指针无法进行位移和运算的,它只是一个存储了其它变量地址的变量而已。它在 Go 语言中最大的作用就是在值传递的过程中减少内存消耗,毕竟一个地址的存储开销是十分少的。

1. 指针的定义和使用

在 Go 语言中指针类型的声明和 C/C++ 类似,都是采用 * 符号来区分正常类型变量和指针变量。

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

import "fmt"

func main() {
str := "Hello Golang!"
var strP *string // 使用 *+变量类型的形式声明一个 string 类型的指针
strP = &str // 将指针 strP 指向变量 str,也就是将变量 str 的地址赋值给指针变量 strP 。& 为取地址符,可以取出变量所在的地址。
fmt.Println("未修改前变量str的值为", str)
fmt.Println("未修改前变量str的地址为", &str)
*strP = "Hello Go!" // 通过指针修改 str 的值
fmt.Println("======通过指针修改后======")
fmt.Println("指针strP的地址为", strP)
fmt.Println("指针strP的指向地址为", *strP)
fmt.Println("变量str的地址为", &str)
fmt.Println("变量str的值为", str)
}

输出结果

1
2
3
4
5
6
7
未修改前变量str的值为 Hello Golang!
未修改前变量str的地址为 0x14000096230
======通过指针修改后======
指针strP的地址为 0x14000096230
指针strP的指向地址为 Hello Go!
变量str的地址为 0x14000096230
变量str的值为 Hello Go!

变量 strP 存储的值就是变量 str 的地址,可以通过使用 *strP 操作符改变其存储的地址所存储的值。但地址是没有改变的,都是0x14000096230

2. 指针作为函数的参数

Go 语言中只有值传递,没有引用传递,因为引用传递设计之初是为了解决函数想要有多个返回值的问题,但是 Go 语言的函数自带多返回值的返回方式。所以想要指针作为参数传入函数中,只能传递地址进入函数进行修改,并不能直接引用指针参数,这一特点就大大降低了 Go 语言指针的使用难度。

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

import "fmt"

func main() {
a := 10
b := 20
fmt.Println("交换前的a=", a, "b=", b)
// 将变量 a 和 b 的地址作为函数 swap 指针参数的值传入函数;
swap(&a, &b)
fmt.Println("交换后的a=", a, "b=", b)
}

func swap(a, b *int) {
// 交换这两个指针保存的地址所保存的值。
*a, *b = *b, *a
}

输出结果

1
2
交换前的a= 10 b= 20
交换后的a= 20 b= 10

3. 总结

  • Go 语言的指针不可以做偏移
  • Go 语言的指针可以看作一个存储地址的特殊变量类型

数组

1. 数组的形式

数组的声明形式形如var 数组名 [数组长度]数组类型,其中数组类型可以是数组本身,也就是数组类型的数组,这样就构成了多维数组。和变量的声明相同,数组在声明时会初始化变量类型的零值

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

import "fmt"

func main() {
var a [2]int // 声明一个长度为2的一维数组,自动初始化为零值
var b [2][2]int // 声明一个2行2列的二维数组。自动初始化为零值
var c = [2]int{1, 2} // 声明并定义一个长度为2,值为[1,2]的数组
var d = [...]int{3, 4} // 使用 ... 表示根据数组后值的长度自动初始化长度。因为后面的值是 [3,4] 数组自动初始化长度为 2
fmt.Println("a的零值", a)
fmt.Println("b的零值", b)
fmt.Println("c的值", c)
fmt.Println("d的值", d)
}

输出结果

1
2
3
4
a的零值 [0 0]
b的零值 [[0 0] [0 0]]
c的值 [1 2]
d的值 [3 4]

2. 数组的比较

在 Go 语言中判断数组是否相等需要比较两个部分。一个是数组的长度是否相等,另一个是数组中存放的值是否顺序和大小完全相同。只要这两个部分相等,则 Go 语言中的两个数组就是相等的。

⚠️:Go 语言中只有类型相同的数组才可以互相比较,且数组没有大小的比较,只能比较是否相等。

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

import (
"fmt"
)

func main() {
var a [2]int
var c = [2]int{1, 2}
var d = [...]int{1, 2}
fmt.Println("a == c ? ", a == c)
fmt.Println("c == d ?", c == d)
}

输出结果

1
2
a == c ?  false
c == d ? true

切片

1. 切片的创建

切片的声明方式和数组类似,写法上看就是声明一个没有长度的数组:var 切片名 []切片类型。其中切片类型可以是切片本身,也就是切片的切片,就构成了多维的切片。

切片在使用之前必须要初始化,它没有零值。声明后它的值是 nil,这是因为它的底层实现是一个指向数组的指针,在你给它存入一个数组的地址之前,它只能是 nil

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

import "fmt"

func main() {
var a []int // 声明一个int类型的切片
fmt.Println("初始化前: ", a)
a = make([]int, 5, 10) // 声明一个长度为5,切片容量为10的切片。其中容量可以不传,默认会和长度相等。长度为切片真正有值的位置,会初始化零值
fmt.Println("初始化后:", a)
a[4] = 5 // 给切片的第 5 个位置赋值
fmt.Println("赋值后: ", a)
a[5] = 6 // 给切片的第 6 个位置赋值,但是切片的长度为5,所以会报越界的错误
fmt.Println("赋值后: ", a)
}

结果如下:

1
2
3
4
5
6
7
8
初始化前:  []
初始化后: [0 0 0 0 0]
赋值后: [0 0 0 0 5]
panic: runtime error: index out of range [5] with length 5

goroutine 1 [running]:
main.main()
/Users/GolandProjects/awesomeProject/hello.go:12 +0x160

2. 切片的截取

切片可以从任意长度开始切,切到任意长度为止,然后这一段拿出来就是一个新的切片。切割形式为

切片名(s)[起始下标(begin):结束下标(end):最大容量(max)]

⚠️:截取到的切片包含起始下标(begin),不包含结束下标(end)。

切片截取形式表:

操作 含义
s[begin?max] 截取切片s从begin到end的数据,构成一个容量为max-begin,长度为begin-end的切片。
s[begin:end] 截取切片s从begin到end的数据,构成一个容量和长度均为begin-end的切片。
s[begin:] 截取切片s从begin到最后的数据,构成一个容量和长度均为len(s)-end的切片。
s[:end] 截取切片s从0到最后的数据,构成一个容量和长度均为end-0的切片。
1
2
3
4
5
6
7
8
9
10
11
package main

import "fmt"

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

输出结果

1
2
3
a[1:3]= [2 3]
a[1:]= [2 3 4 5]
a[:3]= [1 2 3]

3. 切片的追加

切片使用一个 Go 语言的内置函数append(切片,待添加的值),来进行切片末尾元素的追加。

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

import "fmt"

func main() {
var a = []int{1, 2, 3, 4, 5}
a = append(a, 6)
fmt.Println(a)
a = append(a, 7, 8)
fmt.Println(a)
b := []int{9, 10}
a = append(a, b...) // 在切片的末尾追加切片,append中待添加的元素是一个数组或切片的时候,在其后面添加 ... 就可以全部追加到切片末尾
fmt.Println(a)
}

输出结果

1
2
3
[1 2 3 4 5 6]
[1 2 3 4 5 6 7 8]
[1 2 3 4 5 6 7 8 9 10]

4. 切片的长度和容量

在切片中可以使用len()获取切片中元素的数量,也就是切片的长度。使用cap()可以获取切片引用的数组的长度,也就切片的容量。切片的容量一般大于等于长度,容量会随着长度的增长而增长。

在初始化一个切片的时候其实时给切片引用了一个数组,然后容量就是这个数组的长度,然后如果切片的长度超过了切片的容量,它就会让切片引用一个容量更大数组来存放这些元素。

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

import "fmt"

func main() {
var a = []int{1, 2, 3, 4, 5}
fmt.Printf("a的地址%p, a的长度%d, a的容量%d\n", a, len(a), cap(a))
a = append(a, 6)
fmt.Printf("a的地址%p, a的长度%d, a的容量%d\n", a, len(a), cap(a))
a = append(a, 7, 8)
fmt.Printf("a的地址%p, a的长度%d, a的容量%d\n", a, len(a), cap(a))
b := []int{9, 10, 11}
a = append(a, b...)
fmt.Printf("a的地址%p, a的长度%d, a的容量%d\n", a, len(a), cap(a))
}

输出结果

1
2
3
4
a的地址0x140000161b0, a的长度5, a的容量5
a的地址0x140000181e0, a的长度6, a的容量10
a的地址0x140000181e0, a的长度8, a的容量10
a的地址0x14000102000, a的长度11, a的容量20

在切片a每次添加的元素要超过它的容量时,它的地址就会发生改变,其实就是让它引用了一个新的容量更大的数组。

Go语言中的Map

一种元素对的无序集合,每一个**索引(key)对应一个值(value)**,这种数据结构在 Go 语言中被称之为 map

map 是一种能够通过索引(key)迅速找到值(value)的数据结构,所以也被称为字典。在 Go 语言中因为线程安全问题,一共实现了两种类型的 map

1. 无锁的map

这种类型的 map 是线程不安全的 map,多个线程同时访问这个类型的 map 的同一个变量时,会有读写冲突,会导致系统奔溃。所以一般在单线程程序中使用的较多。

1.1 map的创建

map 的底层结构也是一个指针,所以和变量不同,并不是声明后立刻能够使用。和切片相同,需要使用make()函数进行初始化。在初始化之前为空,没有零值。

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

import "fmt"

func main() {
var m map[string]string // 声明一个key为string类型,value为string类型的map变量
fmt.Println(m == nil) // 此时m未初始化,值为 nil
m = make(map[string]string) // 初始化m
fmt.Println(m == nil) //此时m 是一个没有存放数据的map,值不为nil
}

输出结果

1
2
true
false

1.2 map的赋值

map 的赋值有两种方式:

  • 使用:=使map在定义的时候直接赋值;
  • 使用map[key]=value的形式对map进行赋值。

在明确知道 map 的值的时候就可以使用第一种方式进行赋值,比如说在建立中英文对应关系的时候。在未知 map 的取值时,一般建议使用后者进行赋值。

代码示例:

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

import "fmt"

func main() {
m1 := map[string]string{"Apple": "苹果", "Orange": "橘子", "Bannana": "香蕉"} // 在m1被定义的时候直接赋值
fmt.Println(m1["Apple"]) // 输出m1中key为"Apple"时对应的值
m2 := make(map[string]string) //使用:=进行免声明make
m2["Apple"] = "苹果"
m2["Orange"] = "橘子"
m2["Banana"] = "香蕉"
fmt.Println(m2["Apple"]) //输出m2中key为"Apple"时对应的值
}

输出结果

1
2
苹果
苹果

1.3 map的遍历

map 是字典结构,如果不清楚所有 key 的值,是无法对 map 进行遍历的,所以 Go 语言中使用了一个叫做range的关键字,配合for循环结构来对map结构进行遍历。

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

import "fmt"

func main() {
m := map[string]string{"Apple": "苹果", "Orange": "橘子", "Bannana": "香蕉"}
for k, v := range m {
fmt.Println("key:", k, ", value:", v)
}
}

⚠️:map 是无序的,所以每次输出的顺序可能会不一样。

输出结果

1
2
3
key: Apple , value: 苹果
key: Orange , value: 橘子
key: Bannana , value: 香蕉

1.4 map的删除

map 在普通的用法中是无法移除只可以增加 key 和 value 的,所以 Go 语言中使用了一个内置函数delete(map,key)来移除 map 中的 key 和 value。

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

import "fmt"

func main() {
m := map[string]string{"Apple": "苹果", "Orange": "橘子", "Banana": "香蕉"}
fmt.Println(m)
delete(m, "Apple") // 删除 m 中的 “Apple” 和其对应的 value。
fmt.Println(m)
}

输出结果

1
2
map[Apple:苹果 Banana:香蕉 Orange:橘子]
map[Banana:香蕉 Orange:橘子]

2. 自带锁的 sync.Map

这种类型的 map 是线程安全的 map,多个线程同时访问这个类型的 map 的同一个变量时,不会有读写冲突,因为它自带原子锁,保障了多线程的数据安全。

2.1 sync.Map 的创建

这种类型的 map 创建不需要make,直接声明就可以使用,而且不需要声明 map 的 key 和 value 的类型。因为它底层的实现并不是指针,是一种多个变量的聚合类型,叫做结构体

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

import (
"fmt"
"sync"
)

func main() {
var m sync.Map //声明一个 sync.Map
fmt.Println(m) //输出 m 的零值
}

输出结果

1
{{0 0} {[] {} <nil>} map[] 0}

2.2 sync.Map 的操作

这个类型关于 map 的所有操作都是使用它自带的方法来实现的。包括range

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"
"sync"
)

func main() {
var m sync.Map //声明一个 sync.Map
m.Store("Apple", "苹果") // 使用Store 方法给m赋值
m.Store("Orange", "橘子")
m.Store("Banana", "香蕉")
tmp, exist := m.Load("Orange") //使用Load取出"Orange"对应的值,如果不存在"Orange"这个key,exist的值为false
fmt.Println(tmp, exist) //删除m中的"Banana"和其对应的value

m.Delete("Banana")
m.Range(func(k, v interface{}) bool { // 使用Range方法遍历m
fmt.Println("key:", k, ", value:", v)
return true
})
}

输出结果

1
2
3
橘子 true
key: Apple , value: 苹果
key: Orange , value: 橘子

Go语言基础学习(三)
https://suiyideali.github.io/2023/04/28/Go语言基础学习(三)/
作者
m0ch4z
发布于
2023年4月28日
更新于
2023年5月30日
许可协议