Golang 基础-数组、映射、结构体、指针、字符串(第二章)

1. 数组

1. 语法

  • 类型 [n]T 表示拥有 n 个 T 类型的值的数组。
  • 数组的长度是其类型的一部分,因此数组不能改变大小。
  • 数组中的所有元素都被自动赋值为数组类型的零值
  • 数组是值类型而不是引用类型,就是说当数组赋值给一个新的变量或者作为参数传递给函数时,不会改变原始数组
  • 多维数组也可以直接声明或者利用索引声明
    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
    var a [2]string                  // 一般声明
    a[0] = "Hello"
    a[1] = "World"

    a := [3]int{12, 78} // 简略声明,无需将数组中所有的元素赋值

    a := [...]int{12, 78, 50} // 可以忽略声明数组的长度声明
    aa := []int{12} // 如果什么都不写,则声明的是一个数组切片

    a := [3]int{5, 78, 8}
    var b [5]int
    b = a // 报错,数组的大小是类型的一部分,[2]int 和 [5]int 是不同类型

    a := [3][2]string{
    {"lion", "tiger"}, // 逗号是必须的,因为根据 Go 语言的规则自动插入分号
    {"cat", "dog"},
    {"pigeon", "peacock"},
    }

    var b [3][2]string // 多维数组
    b[0][0] = "apple"
    b[0][1] = "samsung"
    b[1][0] = "microsoft"
    b[1][1] = "google"
    b[2][0] = "AT&T"
    b[2][1] = "T-Mobile"

2. 切片

2.1. 概念

切片是组的包装,它本身不拥有任何数据,它只是对现有数组的引用,更改切片的元素会修改其底层数组中对应的元素。

  • []T:表示一个元素类型为 T 的切片。
  • a[start:end]:创建一个从 a 数组索引 start 开始到 end - 1 结束的切片
    1
    2
    3
    4
    5
    letters := [3]string{"a", "b", "c"}
    var b []string := letters[0:2] // 创建切片,语法类似于没有长度的数组

    arr1 := letters[0:1]
    arr1[0] = "d" // 原数组也被修改

2.2. 属性

  • 切片拥有长度容量。可通过表达式 len(s)cap(s) 来获取。容量大于等于长度。
    • 切片的长度就是它所包含的元素个数
    • 切片的容量是从它的第一个元素开始数,到其底层数组元素末尾的个数
  • 切片的零值是 nil,其长度和容量为 0 且没有底层数组
  • 数组下标取值,不能超过 len(s),向后拓展取值,不能超过底层数组 cap(s)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    func main() {
    s := []int{2, 3, 5, 7, 11, 13}
    printSlice(s) // len=6 cap=6 [2 3 5 7 11 13]

    s = s[:0] // 截取切片使其长度为 0
    printSlice(s) // len=0 cap=6 []

    s = s[:4] // 拓展其长度
    printSlice(s) // len=4 cap=6 [2 3 5 7]

    s = s[2:] // 舍弃前两个值
    printSlice(s) // len=2 cap=4 [5 7]
    }

    func printSlice(s []int) {
    fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
    }

2.3. 方法

2.3.1. make

make() 内建函数会分配一个元素为零值的数组并返回一个引用了它的切片

  • a := make([]T, 5):len(a)=5
  • b := make([]T, 0, 5):len(b)=0,,cap(b)=5
2.3.2. append

append() 内建函数的结果是一个包含原切片所有元素加上新添加元素的切片。当原数组的底层数组太小,不足以容纳所有给定的值时,它就会分配一个更大的数组。返回的切片会指向这个新分配的数组。

  • append(s []T, x ... T)
    1
    2
    cars := []string{"Ferrari", "Honda", "Ford"}    // len=3,cap=3
    cars = append(cars, "Toyota") // len=4,cap=6
2.3.2. range

for 循环的 range 形式可遍历切片或映射。
当使用 for 循环遍历切片时,每次迭代都会返回两个值:

  1. 为当前元素的下标,
  2. 为该下标所对应元素的一份副本

可以将下标或值赋予 _ 来忽略它

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var pow = []int{1, 2, 4, 8, 16, 32, 64}

func main() {
for i, v := range pow {
fmt.Printf("2**%d = %d\n", i, v) // 输出 2**0 = 1 等
}
}

// 使用 “_” 来忽略某些元素
func main() {
pow := make([]int, 10)
for i := range pow {
pow[i] = 1 << uint(i) // == 2**i
}
for _, value := range pow {
fmt.Printf("%d\n", value) // 输出2的幂等
}
}
2.3.3. 复制切片

copy(dest, src) 内建函数,将 src 切片复制给 dest 切片

2.3.4. 删除切片元素

没有删除的内建函数,但可以通过 append 来实现删除

1
2
// 删除第3个元素,由于 append 后面接收变长个元素,使用 ... 解包切片为元素
dest = append(src[:3], src[4:]...)

2. 映射

1. 语法

  • map 的语法与结构体相似,不过必须有键名
  • map 的零值是 nil。如果想添加元素到 nil map 中,会触发运行时 panic。因此 map 必须使用 make 函数初始化
    1
    2
    3
    4
    5
    6
    7
    // 初始化 map
    m1 := make(map[string]int) // 不指定容量初始化
    m2 := make(map[string]int, 100) // 指定100容量初始化
    m3 := map[string]string {
    "name":"Tom",
    "gender":"male"
    }

2. 方法

2.1. 初始化

make 函数会返回给定类型的映射:make(map[K]V)

2.2. 获取元素

  • elem = m[key] 获取不存在的元素,会返回 value 类型的零值
  • elem, ok := m[key] 可以检测键是否存在
  • 遍历 map 中所有的元素需要用 for range 循环
1
2
3
4
5
6
7
8
9
10
value, ok := example3Map["first"]
if ok == true {
fmt.Println("found", value)
} else {
fmt.Println("not found", value)
}

for key, value := range example3Map {
fmt.Printf("example3Map[%s] = %d\n", key, value)
}

2.3. 添加元素

  • 可以在声明的同时添加元素
  • 添加新元素方式与数组相同 m[key] = elem
1
2
3
4
5
example3Map := map[string]int {             // 可以在声明的同时添加元素
"first": 1,
"secend": 2,
}
example3Map["third"] = 3 // 添加新元素方式与数组相同

2.4. 删除元素

  • delete(m, key) 函数无返回值

2.5. 获取长度

  • len(example3Map) 返回 map 长度

3. 特性

3.1. Map 是引用类型

和 slices 类似,map 也是引用类型。当 map 被赋值为一个新变量的时候,它们指向同一个内部数据结构。因此,改变其中一个变量,就会影响到另一变量。

1
2
3
4
5
6
example3Map := map[string]int {
"first": 1,
"secend": 2,
}
example4Map := personSalary
example4Map["first"] = 0 // 会引起原 map 也变化

3.2. map 的相等性

map 之间不能使用 == 操作符判断,== 只能用来检查 map 是否为 nil。判断两个 map 是否相等的方法是遍历比较两个 map 中的每个元素。

1
2
3
4
5
6
7
8
9
10
map1 := map[string]int{
"one": 1,
"two": 2,
}

map2 := map1

if map1 == map2 { // 抛出编译错误

}

3. 结构体

1. 语法

  • type 可以声明一个命名结构体,也可以不用,此时则创建一个匿名结构体
  • 结构体可以把相同类型的字段声明放在同一行
  • 未被显式地初始化字段时,该结构体的字段将默认赋为零值
  • 结构体字段使用点号.来访问
  • 结构体可以用指针来访问。可以通过 (*p).x 来访问其字段 x,或者隐式间接引用p.x
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
29
30
31
32
33
34
35
36
37
38
39
40
41
// 声明
type Vertex struct { // 可以声明一个新命名结构体 Vertex
x int
y int
z int
}
var vertex struct { // 创建一个匿名结构体
x int
y int
z int
}

type Vertex struct { // 相同类型可以一起声明
x, y, z int
}

// 初始化结构体变量
v1 := Vertex{ // 使用字段
x: 1,
y: 2,
z: 3,
}
v2 := Vertex{2, 3, 4} // 不使用字段
v3 := Vertex{3} // 等同于赋值 x:1, y:0, z=0
v3.y = 4
fmt.Println("Vertex:", v3) // {3 4 0}

vv1 := struct { // 匿名结构体变量
x int
y int
z int
}{
x:1,
y:2,
z:3,
}

// 结构体的指针
p4 := &Vertex{4, 5, 6} // 创建 Vertex 的指针
fmt.Println("y:", (*p4).y)
fmt.Println("z:", p4.z)

2. 特性

2.1. 匿名字段

创建结构体时,字段可以只有类型,而没有字段名,这样的字段称为匿名字段。
虽然匿名字段没有名称,但其实匿名字段的名称就默认为它的类型。如下,可认为 Person 结构体有两个名为 string 和 int 的字段。

1
2
3
4
5
6
7
8
9
type Person struct {    // 带匿名字段的结构体
string
int
}
func main() {
var p1 Person
p1.string = "hello"
p1.int = 2021
}

2.2. 嵌套结构体

结构体的字段有可能也是一个结构体。这样的结构体称为嵌套结构体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type Address struct {  
city, state string
}
type Person struct {
name string
age int
address Address
}

func main() {
var p Person
p.name = "hello"
p.age = 2021
p.address = Address {
city: "Chicago",
state: "Illinois",
}
}

2.3. 提升字段

如果是结构体中有匿名的结构体类型字段,则该匿名结构体里的字段就称为提升字段。
这是因为提升字段就像是属于外部结构体一样,可以用外部结构体直接访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type Address struct {  
city, state string
}
type Person struct {
name string
age int
Address // 匿名字段,Address 有 city 和 state 两个字段,这两个字段就称为提升字段可以直接访问
}

func main() {
var p Person
p.name = "hello"
p.age = 2021
p.address = Address {
city: "Chicago",
state: "Illinois",
}
fmt.Println("City:", p.city) // 提升字段
fmt.Println("State:", p.state) // 提升字段
}

2.4. 导出结构体和字段

如果结构体名称以大写字母开头,则它是其他包可以访问的导出类型。
同样,如果结构体里的字段首字母大写,它也能被其他包访问到。

2.5. 结构体相等性

结构体是值类型。如果它的每一个字段都是可比较的,则该结构体也是可比较的。
如果两个结构体变量的对应字段相等,则这两个变量也是相等的。
如果结构体包含不可比较的字段,则结构体变量也不可比较。

4. 指针

1. 声明

指针是一种存储变量内存地址(Memory Address)的变量

  • *T:指针变量的类型为 *T,该指针指向一个 T 类型的变量。其零值为 nil
  • &:获取变量的地址
  • *指针变量:获取指针所指向的变量的值
1
2
3
4
5
6
7
func main() {
b := 255
var a *int = &b // 把 b 的地址赋值给 *int 类型的 a
fmt.Printf("Type of a is %T\n", a) // Type of a is *int
fmt.Println("address of b is", b) // address of b is 0x1040a124
fmt.Println("value of b is", *a) // value of b is 255
}

2. 注

  1. 函数传递可以使用指针参数
  2. 向函数传递数组的指针,不是 Go 语言惯用方式,而应该使用切片
  3. Go 不支持指针运算
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    func change(val *int) {     // 利用指针传递,修改原参数值
    *val = 55
    }

    func modify(arr *[3]int) { // 传递数组的指针,修改原数组值
    (*arr)[0] = 90 // 可简写为 arr[0] = 90
    }
    func modify(sls []int) { // 传递数组的切片
    sls[0] = 90
    }

5. 字符串

字符串是一个字节切片。Go 中的字符串是兼容 Unicode 编码的,并且使用 UTF-8 进行编码。

1. 构建字符串

1.1. 双引号

内容放在双引号””之间,可以创建一个字符串

1.2. 字节切片

1
2
3
4
byteSlice1 := []byte{0x43, 0x61, 0x66, 0xC3, 0xA9}  // 16 进制字节
byteSlice2 := []byte{67, 97, 102, 195, 169} // 10 进制字节
str1 := string(byteSlice1)
str2 := string(byteSlice2)

1.3. rune 切片

1
2
runeSlice := []rune{0x0053, 0x0065, 0x00f1, 0x006f, 0x0072} // 16 进制的 Unicode 代码点
str := string(runeSlice)

2. 遍历字符串

2.1. rune

直接使用切片访问,其会按照每个字节来打印,但是有的字符占据不止一个字节,此时必须使用 rune ,其是 int32 的别称,表示一个代码点。代码点无论占用多少个字节,都可以用一个 rune 来表示。

1
2
3
4
5
6
7
8
9
10
11
12
func printBytes(s string) {
for i:= 0; i < len(s); i++ {
fmt.Printf("%x ", s[i])
}
}

func printChars(s string) {
runes := []rune(s) // 防止多字节字符,解析错误
for i:= 0; i < len(runes); i++ {
fmt.Printf("%c ",runes[i])
}
}

2.2. for rang

1
2
3
4
5
func printCharsAndBytes(s string) {
for index, rune := range s {
fmt.Printf("%c starts at byte %d\n", rune, index)
}
}

3. 注意

3.1. 长度

utf8 package 包中的 func RuneCountInString(s string) (n int) 方法用来获取字符串的长度,返回字符串中的 rune 的数量。

1
2
3
4
5
6
7
8
9
10
11
12
13
import (  
"fmt"
"unicode/utf8"
)
func length(s string) {
fmt.Printf("length of %s is %d\n", s, utf8.RuneCountInString(s))
}
func main() {
word1 := "Señor"
length(word1) // length of Señor is 5
word2 := "Pets"
length(word2) // length of Pets is 4
}

3.2. 字符串不可变

Go 中的字符串是不可变的。一旦一个字符串被创建,那么它将无法被修改。

1
2
3
4
5
6
7
8
func mutate(s string)string {  
s[0] = 'a' // 报错,cannot assign to s[0]
return s
}
func main() {
h := "hello"
fmt.Println(mutate(h))
}

为了修改字符串,可以把字符串转化为一个 rune 切片。然后这个切片可以进行任何想要的改变,然后再转化为一个字符串。

1
2
3
4
5
6
7
8
func mutate(s []rune) string {  
s[0] = 'a'
return string(s)
}
func main() {
h := "hello"
fmt.Println(mutate([]rune(h))) // 输出,aello
}