Golang学习笔记(02-4数据类型-高级数据类型)

156 阅读22分钟

1. 数组的定义

数组是一个由固定长度的特定元素组成的序列,一个数组可以由零个或者多个元素组成, **数组的长度是数组类型的组成部分, 因此不同长度或者不同元素类型组成的数组属于不同的类型,不同长度的数组因为类型不同,不能直接赋值!**也因为如此,数组一般在不需要赋值和修改的场景中使用!

1.1. 数组的声明

package main

import "fmt"
// 数组的定义和默认值
func main() {
	var a1 [4]int8
	fmt.Printf("%T\t\t%v\n", a1, a1)
	var a2 [4]float32
	fmt.Printf("%T\t%v\n", a2, a2)
	// fmt.Println(a1 == a2) // 不同数据类型的数组不能比较
	var a3 [3]string
	fmt.Printf("%T\t%v\n", a3, a3)
	var a4 [4]bool
	fmt.Printf("%T\t\t%v\n", a4, a4)
}
e:\OneDrive\Projects\Go\src\gitee.com\studygo\day02>go run main.go 
[4]int8         [0 0 0 0]
[4]float32      [0 0 0 0]
[3]string       [  ]
[4]bool         [false false false false]

1.2. 数组的初始化

数组初始化,其实就是在定义数组的时候进行赋值,主要有以下三种方式

1.2.1. 指定长度和值

package main

import "fmt"

func main() {
	var a1 = [3]int8{1, 3, 5}
	var a2 = [4]string{"北京", "深圳", "上海"}  // 没有赋值的元素会采用默认值
	a3 := [3]bool{true, true, false}
	fmt.Printf("%T\t%v\n", a1, a1)
	fmt.Printf("%T\t%v\n", a2, a2)
	fmt.Printf("%T\t%v\n", a3, a3)
}
e:\OneDrive\Projects\Go\src\gitee.com\studygo\day02>go run main.go
[3]int8 [1 3 5]
[4]string       [北京 深圳 上海 ]
[3]bool [true true false]

1.2.2. 仅指定值

package main

import "fmt"

func main() {
	a1 := [...]int{1, 2, 3, 4, 5}    // 使用[...]表示编译器会自动根据元素的数量来确定长度
	a2 := [...]string{"a", "b", "c"}
	fmt.Printf("%T\t\t%v\n", a1, a1)
	fmt.Printf("%T\t%v\n", a2, a2)
}
e:\OneDrive\Projects\Go\src\gitee.com\studygo\day02>go run main.go
[5]int          [1 2 3 4 5]
[3]string       [a b c]

1.2.3. 使用索引来赋值

package main

import "fmt"

func main() {
	a1 := [5]float32{1.24, 3: 3.14, 4: 9.99} // 根据索引确定元素的值,未指定的元素使用默认值
	a2 := [...]bool{6: true}                 // 数组的长度与索引相关
	fmt.Printf("%T\t%v\n", a1, a1)
	fmt.Printf("%T\t\t%v\n", a2, a2)
}
e:\OneDrive\Projects\Go\src\gitee.com\studygo\day02>go run main.go
[5]float32      [1.24 0 0 3.14 9.99]
[7]bool         [false false false false false false true]

1.3. 数组的访问

可以通过数组的索引来访问数组中的值,在遍历的时候可以通过 for 循环配合索引来取值,也可以通过 range 实现遍历。

package main

import "fmt"

func main() {
	a1 := [5]string{"北京", "深圳", "上海", "杭州", "合肥"}
	fmt.Println(a1[0], a1[2], a1[4]) // 根据索引直接访问
	for i := 0; i < len(a1); i++ {   // 根据索引遍历
		fmt.Printf("%d:%v\t", i, a1[i])
	}
	fmt.Println()
	for i, v := range a1 { // 根据range遍历
		fmt.Printf("%d:%v\t", i, v)
	}
	fmt.Println()
}
e:\OneDrive\Projects\Go\src\gitee.com\studygo\day02>go run main.go
北京 上海 合肥
0:北京  1:深圳  2:上海  3:杭州  4:合肥
0:北京  1:深圳  2:上海  3:杭州  4:合肥

1.4. 多维数组

所谓多维数组,其实就是嵌套数组,和Python中嵌套列表类似。需要注意的是,定义 [n][m]type{} 的时候只能外层使用 [...] ,即 [...][m]type{} 

package main

import "fmt"

func main() {
	a1 := [4][2]int8{
		[2]int8{99, 100},
		[2]int8{9, 10}, // , 需要加上
	}
	fmt.Printf("%T\t%v\n", a1, a1)

	a2 := [...][2]string{ // 声明数组时,只能外层使用 [...]
		[...]string{"南京", "苏州"},
		[...]string{"合肥", "芜湖"},
		[...]string{"杭州", "宁波"},
		[...]string{"武汉", "黄冈"},
	}
	fmt.Printf("%T\t%v\n", a2, a2)
	fmt.Println("---------------------------")
	fmt.Println(a2[2][1])
	fmt.Println("---------------------------")
	for i := 0; i < len(a2); i++ {
		fmt.Printf("%v\t", a2[i])
		for j := 0; j < len(a2[i]); j++ {
			fmt.Printf("%v\t", a2[i][j])
		}
		fmt.Println()
	}
}
e:\OneDrive\Projects\Go\src\gitee.com\studygo\day02>go run main.go
[4][2]int8      [[99 100] [9 10] [0 0] [0 0]]
[4][2]string    [[南京 苏州] [合肥 芜湖] [杭州 宁波] [武汉 黄冈]]
---------------------------
宁波
---------------------------
[南京 苏州]     南京    苏州
[合肥 芜湖]     合肥    芜湖
[杭州 宁波]     杭州    宁波
[武汉 黄冈]     武汉    黄冈

1.5. 数组的数据结构

1.5.1. 数据结构

Golang中的数组在初始化时会开辟一块连续的内存地址用来存储相同类型元素,数组不是Python中指针引用类型,而是值类型,数组的长度一旦固定就不能再修改。采用 [n]int8 {}  和 [...]int8{}  的方式定义相同值的数组时,虽然结果一致,当时内部的初始化方式不一致,后者需要通过值来计算出数组的长度。
Go中数组是值类型(值语义),即一个数组变量表示整个数组的值,而不是指向数组的指针,是一个完整的值。因此在赋值、传递时,是拷贝了整个数组,因此在赋值或者传递时,建议使用指针!

1.5.2. 数组拷贝

package main

import "fmt"

func main() {
	a1 := [...][2]string{
		[...]string{"南京", "苏州"},
		[...]string{"合肥", "芜湖"},
		[...]string{"杭州", "宁波"},
		[...]string{"武汉", "黄冈"},
	}
	a1[2][1] = "衢州" // 直接通过索引来修改值
	a1[3] = [...]string{"济南", "德州"}
	fmt.Println(a1)
	fmt.Println("---------------------------")
	var a2 = a1
	a1[3] = [...]string{"武汉", "黄冈"} // Go中这种数组是基于值进行拷贝的,不是指针,因此不存在Python中深浅拷贝问题
	fmt.Println("a1:", a1)
	fmt.Println("a2:", a2)
}
e:\OneDrive\Projects\Go\src\gitee.com\studygo\day02\01-array>go run main.go
[[南京 苏州] [合肥 芜湖] [杭州 衢州] [济南 德州]]
---------------------------
a1: [[南京 苏州] [合肥 芜湖] [杭州 衢州] [武汉 黄冈]]
a2: [[南京 苏州] [合肥 芜湖] [杭州 衢州] [济南 德州]]

1.5.3. 数组修改

数组直接使用索引完成修改就可以了,如果数组作为实参传递给函数,那么函数内部收到的参数是实参的一个拷贝,和实参属于不同的内存空间。

package main

import (
	"fmt"
	"strings"
)

func f0(arr [3]int) [3]int { // 值传递
	arr[0] = arr[1] + arr[1] // 不会影响实参 arr
	return arr
}

func f1(arr *[3]int) [3]int { // 指针传递
	arr[0] = arr[1] + arr[1]  // 会影响实参 arr
	return *arr
}

func main() {
	var arr = [...]int{1, 2, 3}
	fmt.Println("未修改情况: ", arr)
	fmt.Println(strings.Repeat("-",30))

	arr[0] = 100  // 通过索引直接去修改数组中的值
	fmt.Println("通过索引直接修改: ", arr)
	fmt.Println(strings.Repeat("-",30))

	fmt.Println(f0(arr))  // 将数组作为实参传递给函数,函数内部修改的数组是 arr 的拷贝
	fmt.Println(arr)
	fmt.Println(strings.Repeat("-",30))

	fmt.Println(f1(&arr))  // 将数组的指针作为实参传递给函数,函数内部形参为指针的拷贝,指向的还是 arr
	fmt.Println(arr)
}
[root@heyingsheng day02]# go run 09-arry/main.go
未修改情况:  [1 2 3]
------------------------------
通过索引直接修改:  [100 2 3]
------------------------------
[4 2 3]
[100 2 3]
------------------------------
[4 2 3]
[4 2 3]

1.6. 案例

1.6.1. 创造A-Z字符数组

创建一个byte 类型的26 个元素的数组,分别放置'A'-'Z',使用for 循环访问所有元素并打印出来。

package main

import "fmt"

func main()  {
	var res [26]byte
	for i:=0; i<26; i++ {
		res[i] = 'A' + byte(i)
	}

	for _, v := range res {
		fmt.Printf("%c ", v)
	}
	fmt.Println()
}
[root@heyingsheng 09-arry]# go run ex1.go
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z

2. 切片

Go中数组长度固定,不能做删除和追加的操作,大部分场景仅适合读操作,不适合写场景。因此Go中采用切片(slice)解决这一问题。Slice是对一个底层的数组进行封装,然后根据指针和长度来显示需要的切片内容。切片是相同类型的可变序列

2.1. 切片的定义

2.1.1. 声明和初始化切片

切片的声明和数组非常相似,区别在于不需要指定长度,切片的长度是可以增加的,切片三个重要概念:

  • 数组指针:当前切片的第一个元素在底层数组中对应元素的内存地址
  • 切片长度:当前切片中元素的个数,用 len(s) 获取
  • 切片容量:当前切片中第一个元素到底层数组中最后一个元素之间的元素个数,用 cap(s) 获取,容量大于等于长度
package main

import "fmt"

func main() {
	var s1 []uint8
	var s2 = []bool{false, true, true}
	var s3 = []string{"北京", "上海"}
	var s4 = []string{"北京", "上海"}
	fmt.Printf("s1 type:%T, value:%v, len:%d, cap:%d\n", s1, s1, len(s1), cap(s1))
	fmt.Printf("s2 type:%T, value:%v, len:%d, cap:%d\n", s2, s2, len(s2), cap(s2))
	fmt.Printf("s3 type:%T, value:%v, len:%d, cap:%d\n", s3, s3, len(s3), cap(s3))
	// 切片的判断
	// fmt.Print("s3 == s4 %v\n", s3 == s4)   //	slice can only be compared to nil
	fmt.Printf("s1 == nil ? %v\n", s1 == nil) // nil 表示声明但是未开辟内存空间
	fmt.Printf("s4 == nil ? %v\n", s4 == nil)
}
e:\OneDrive\Projects\Go\src\gitee.com\studygo\day02\02-slice>go run main.go
s1 type:[]uint8, value:[], len:0, cap:0
s2 type:[]bool, value:[false true true], len:3, cap:3
s3 type:[]string, value:[北京 上海], len:2, cap:2
s1 == nil ? true
s4 == nil ? false

2.1.2. 使用数组定义切片

  • 与Python类似,采用数组的索引来切割数组得到切片,该切片的长度和容量可能不一样。
  • Go不支持负数索引和步长
  • 切片是针对底层数组进行封装的,一旦底层数组发生变化,引用该底层数组的切片值也会变化
  • 切片的切片是上层切片对下层切片的再次封装,即下层切片的第一个元素对上层切片而言索引为0,上层切片索引不能超过下层切片的容量!但是这两个切片指向的底层数据都是同一个
package main

import "fmt"

func main() {
	a1 := [5]string{"北京", "深圳", "上海", "杭州", "合肥"}
	s1 := a1[:]
	s2 := a1[:4]
	// s3 := a1[:-1] // index must be non-negative
	s3 := a1[2:len(a1)]
	fmt.Printf("s1 type:%T, value:%v, len:%d, cap:%d\n", s1, s1, len(s1), cap(s1))
	fmt.Printf("s2 type:%T, value:%v, len:%d, cap:%d\n", s2, s2, len(s2), cap(s2))
	fmt.Printf("s3 type:%T, value:%v, len:%d, cap:%d\n", s3, s3, len(s3), cap(s3))
	fmt.Println("-----------------------------------------------------")
	a1[2] = "重庆" // 底层数组发生变化,上层切片的值都会变化
	fmt.Printf("s1 type:%T, value:%v, len:%d, cap:%d\n", s1, s1, len(s1), cap(s1))
	fmt.Printf("s2 type:%T, value:%v, len:%d, cap:%d\n", s2, s2, len(s2), cap(s2))
	fmt.Printf("s3 type:%T, value:%v, len:%d, cap:%d\n", s3, s3, len(s3), cap(s3))
	fmt.Println("-----------------------------------------------------")
	s4, s5 := s3[1:3], a1[1:4][1:] // 切片的切片
	// s3[1:3] --> [重庆 杭州 合肥][1:3] --> [杭州 合肥]
	// a1[1:4][1:] --> [深圳 重庆 杭州][1:]  --> [重庆 杭州]
	// s4 := s3[1:4]  // panic: runtime error: slice bounds out of range [:4] with capacity 3
	fmt.Printf("s4 type:%T, value:%v, len:%d, cap:%d\n", s4, s4, len(s4), cap(s4))
	fmt.Printf("s5 type:%T, value:%v, len:%d, cap:%d\n", s5, s5, len(s5), cap(s5))
}
e:\OneDrive\Projects\Go\src\gitee.com\studygo\day02\02-slice>go run main.go
s1 type:[]string, value:[北京 深圳 上海 杭州 合肥], len:5, cap:5
s2 type:[]string, value:[北京 深圳 上海 杭州], len:4, cap:5
s3 type:[]string, value:[上海 杭州 合肥], len:3, cap:3
-----------------------------------------------------
s1 type:[]string, value:[北京 深圳 重庆 杭州 合肥], len:5, cap:5
s2 type:[]string, value:[北京 深圳 重庆 杭州], len:4, cap:5
s3 type:[]string, value:[重庆 杭州 合肥], len:3, cap:3
-----------------------------------------------------
s4 type:[]string, value:[杭州 合肥], len:2, cap:2
s5 type:[]string, value:[重庆 杭州], len:2, cap:3

2.1.3. 使用make构造切片

make() 函数构造切片时的格式: make(type, len[, cap]) ,cap可以省略,如果cap省略则和len一致!
make()方式构造的slice也有一个对应的底层数组,数组为默认的零值,但是这个数组没有属于自己的变量名,只能使用slice去访问,数组本身对外不可见。

package main

import "fmt"

func main() {
	s0 := make([]string, 0)
	s1 := make([]int8, 5)
	s2 := make([]bool, 5, 10)
	fmt.Printf("s0\ttype:%T\tvalue:%v\tlen:%d\tcap:%d\n", s0, s0, len(s0), cap(s0))
	fmt.Printf("s1\ttype:%T\tvalue:%v\tlen:%d\tcap:%d\n", s1, s1, len(s1), cap(s1))
	fmt.Printf("s2\ttype:%T\tvalue:%v\tlen:%d\tcap:%d\n", s2, s2, len(s2), cap(s2))
	fmt.Println("--------------------------------")
	fmt.Printf("s0 == nil ? %v", s0 == nil)
}
e:\OneDrive\Projects\Go\src\gitee.com\studygo\day02\02-slice>go run main.go
s0      type:[]string   value:[]        len:0   cap:0
s1      type:[]int8     value:[0 0 0 0 0]       len:5   cap:5
s2      type:[]bool     value:[false false false false false]   len:5   cap:10
--------------------------------
s0 == nil ? false

2.2 切片的本质

切片是对底层数组的封装,包含了三个信息:指向底层数组的指针、切片长度(len)、切片容量(cap)。切片中的指针指向当前切片的第一个元素在底层数组中对应元素的内存地址。
image.png 
image.png

image.png

package main

import (
	"fmt"
	"strings"
)

func main() {
	m0 := [...]int{1, 2, 3, 4, 5, 6, 7}
	s0 := m0[1:4] // {2,3,4}
	fmt.Printf("m0=%v\n", m0)
	fmt.Printf("s0: value=%v len=%d cap=%d\n", s0, len(s0), cap(s0))
	fmt.Printf("m0[1]的地址:%p; s0[0]的地址%p\n", &m0[1], &s0[0])  // 两个指针地址相同
	fmt.Println(strings.Repeat("---", 10))
	// 通过slice 值的修改, 由于切片是指针类型,因此修改的就是原来数组的值!
	s0[1] = 100
	fmt.Printf("m0=%v\n", m0)
	fmt.Printf("s0: value=%v len=%d cap=%d\n", s0, len(s0), cap(s0))
	fmt.Printf("m0[1]的地址:%p; s0[0]的地址%p\n", &m0[1], &s0[0]) 
}
[root@heyingsheng 10-slice]# go run main.go
m0=[1 2 3 4 5 6 7]
s0: value=[2 3 4] len=3 cap=6
m0[1]的地址:0xc000010288; s0[0]的地址0xc000010288
------------------------------
m0=[1 2 100 4 5 6 7]
s0: value=[2 100 4] len=3 cap=6
m0[1]的地址:0xc000010288; s0[0]的地址0xc000010288

2.3. 切片的访问

切片的访问和遍历的方式与数组一致:

package main

import "fmt"

func main() {
	// 切片的访问
	a0 := [...]string{"北京", "深圳", "上海", "杭州", "合肥"}
	s0 := a0[1:]                      // []string{"深圳", "上海", "杭州", "合肥"}
	fmt.Println(s0[0], s0[len(s0)-1]) // 使用索引访问
	for _, v := range s0 {            // 使用 range 迭代
		fmt.Printf("%v\t", v)
	}
	fmt.Println()
	for i := 0; i < len(s0); i++ { // 使用索引变量
		fmt.Printf("%v\t", s0[i])
	}
	fmt.Println()
}
e:\OneDrive\Projects\Go\src\gitee.com\studygo\day02\02-slice>go run main.go
深圳 合肥
深圳    上海    杭州    合肥
深圳    上海    杭州    合肥

2.4. 切片的操作

2.4.1. 扩容

使用切片的目的在于解决数组定长问题,切片可以随时扩容,切片的扩容采用的是 append()  函数,注意以下几点:

  • append只能添加一个或多个基础数据类型或者是切片到已有切片中,即append()只能给切片扩容
  • 如果添加的是切片,必须要使用 s...  将其打散(类似于Python中 *args 解包),本质还是添加多个基础数据
  • 如果要添加数组中的元素到已有切片,需要先转换为切片再解包,即 a[:]... 
  • 当append追加的元素超过切片容量时,切换会更换底层数组的内存地址

注意:append 操作可能会需要更换底层数组,导致重新分配内存,需要合理规划切片容量,避免性能损失!切片的高性能的实现方式在于降低内存的重新分配,即尽量保证不超过 cap 范围

package main

import "fmt"

func main() {
	// 声明的nil切片
	var s0 []int
	for i := 0; i < 10; i++ {
		s0 = append(s0, i)
		fmt.Printf("s0 --> value:%v\tcap:%d\tpointer:%p\n", s0, cap(s0), s0)
	}
	fmt.Println("--------------------------------")
	// 使用数组生成的切片
	var a1 = [...]string{"北京"}
	s1 := a1[:]
	for _, v := range [...]string{"北京", "深圳", "上海", "杭州", "合肥", "武汉", "哈尔滨", "乌鲁木齐"} {
		s1 = append(s1, v)
		fmt.Printf("s1 --> value:%v\tcap:%d\tpointer:%p\n", s1, cap(s1), s1)
	}
	fmt.Println("--------------------------------")
	// append 添加其它切片到已有切片中
	var s2 []bool
	// seq2 := []bool{true, false, true, true}
	a2, seq2 := [...]bool{true, true, true}, []bool{false, false, false}
	// s2 = append(s2, a2...)  // cannot use a2 (type [4]bool) as type []bool in append // 注意不能直接添加数组到切片中
	s2 = append(s2, a2[:]...)
	fmt.Printf("s2 --> value:%v\tcap:%d\tpointer:%p\n", s2, cap(s2), s2)
	s2 = append(s2, seq2...)
	fmt.Printf("s2 --> value:%v\tcap:%d\tpointer:%p\n", s2, cap(s2), s2)
}
e:\OneDrive\Projects\Go\src\gitee.com\studygo\day02\03-append>go run main.go
s0 --> value:[0]        cap:1   pointer:0xc0000120b0
s0 --> value:[0 1]      cap:2   pointer:0xc0000120f0
s0 --> value:[0 1 2]    cap:4   pointer:0xc000010400
s0 --> value:[0 1 2 3]  cap:4   pointer:0xc000010400
s0 --> value:[0 1 2 3 4]        cap:8   pointer:0xc00000e200
s0 --> value:[0 1 2 3 4 5]      cap:8   pointer:0xc00000e200
s0 --> value:[0 1 2 3 4 5 6]    cap:8   pointer:0xc00000e200
s0 --> value:[0 1 2 3 4 5 6 7]  cap:8   pointer:0xc00000e200
s0 --> value:[0 1 2 3 4 5 6 7 8]        cap:16  pointer:0xc00010c100
s0 --> value:[0 1 2 3 4 5 6 7 8 9]      cap:16  pointer:0xc00010c100
--------------------------------
s1 --> value:[北京 北京]        cap:2   pointer:0xc000004740
s1 --> value:[北京 北京 深圳]   cap:4   pointer:0xc000050040
s1 --> value:[北京 北京 深圳 上海]      cap:4   pointer:0xc000050040
s1 --> value:[北京 北京 深圳 上海 杭州] cap:8   pointer:0xc000110000
s1 --> value:[北京 北京 深圳 上海 杭州 合肥]    cap:8   pointer:0xc000110000
s1 --> value:[北京 北京 深圳 上海 杭州 合肥 武汉]       cap:8   pointer:0xc000110000
s1 --> value:[北京 北京 深圳 上海 杭州 合肥 武汉 哈尔滨]        cap:8   pointer:0xc000110000
s1 --> value:[北京 北京 深圳 上海 杭州 合肥 武汉 哈尔滨 乌鲁木齐]       cap:16  pointer:0xc000114000
--------------------------------
s2 --> value:[true true true]   cap:8   pointer:0xc000012330
s2 --> value:[true true true false false false] cap:8   pointer:0xc000012330

image.png
切片底层数组扩容方式可以在源码 $GOROOT/src/runtime/slice.go中查看到:

func growslice(et *_type, old slice, cap int) slice {
    ......
    newcap := old.cap
    doublecap := newcap + newcap
    // 当申请的容量大于原来的两倍时,则使用新申请的容量
    if cap > doublecap {
        newcap = cap
    } else {
        // 如果老的cap小于1024,则新的cap翻一翻
        if old.cap < 1024 {
            newcap = doublecap
        } else {
            // Check 0 < newcap to detect overflow
            // and prevent an infinite loop.
            for 0 < newcap && newcap < cap {
                newcap += newcap / 4
            }
            // Set newcap to the requested cap when
            // the newcap calculation overflowed.
            if newcap <= 0 {
                newcap = cap
            }
        }
    }
    ......
}

2.4.2. 删除切片中的元素

切片的底层是数组,而一个已经定义的数组是定长的,无法删除其中的元素,因此删除切片的数据,其实是修改底层数组,使得切片看上去实现了元素删除。删除切片元素的方式如下:

  • 删除索引为 n 的元素 s = append(s[:n], s[n+1:]...) 
  • 如果要切片删除多个元素,建议从后往前删除,即先修改较大的索引,然后再修改较小的索引
  • 删除切片中的元素后,原来的数组也发生了变化,会导致所有引用该底层数组的切片值都可能会变化
package main

import "fmt"

func main() {
	// 删除切片中的元素
	a0 := [...]string{"北京", "深圳", "上海", "杭州", "合肥", "武汉", "哈尔滨", "乌鲁木齐"}
	s0 := a0[:]
	// 删除索引为3和5的元素, 即删除杭州和武汉
	s0 = append(s0[:5], s0[6:]...)
	s0 = append(s0[:3], s0[4:]...)
	fmt.Println(s0)
	fmt.Println(a0)
}
e:\OneDrive\Projects\Go\src\gitee.com\studygo\day02\03-append>go run main.go
[北京 深圳 上海 合肥 哈尔滨 乌鲁木齐]
[北京 深圳 上海 合肥 哈尔滨 乌鲁木齐 乌鲁木齐 乌鲁木齐]

2.4.3. 切片的复制

内置函数 copy(dst, src []type) 可以将来源切片复制到目标切片中,也能将字节从字符串复制到字节切片中,在执行copy()中,不会扩容目标切片的长度,和初始化

  • 由于cpoy() 不会初始化目标切片,因此如果目标切片为nil,则拷贝赋值后,仍然是nil
  • 如果目标切片原有长度小于源切片,则仅拷贝 source[:len(dest)] 值
  • copy命令执行后,两个切片底层数组各不相同,仅仅是值的拷贝
package main

import "fmt"

func main() {
	x1 := [...]int{1, 2, 3, 4, 5, 6, 7}
	s1 := x1[:]
	s2 := make([]int,0,4)
	copy(s2,s1)
	fmt.Printf("%v %T %p\n", s1, s1 ,s1)
	fmt.Printf("%v %T %p\n", s2, s2 ,s2)
}
[root@duduniao go-learn]# go run class01/main.go 
[1 2 3 4 5 6 7] []int 0xc000020080
[] []int 0xc0000180a0

2.4.4. 移除切片中不必要的元素

这种方式,可以降低重新分配内存的次数,进而提高性能。

func filter(src []byte, f func(s byte) bool) []byte {
	res := src[:0]
	for _, v := range src {
		if f(v) {
			res = append(res, v)
		}
	}
	return res
}

3. map

Go语言中的map与Python中的字典类似,都是key/value方式的数据类型,采用的是hash散列方式存储,可以随时扩容。map的key是唯一的,一般常用string或者int作为key,value则种类比较多。

  • map是乱序的
  • key不能重复,一般为 int 或者 string
  • map是引用类型(函数内修改需要注意)

3.1. map的定义方式

定义map有两种方式,声明式和make()构造,其中声明的nil map不能赋值,因为没有开辟内存空间。在部分场景中,需要设置一个只读的map类型,即其内容是固定的,不需要后期修改的,这种采用声明式创建更加合适。如果需要可写的map,则建议使用make()构造,估算需要使用的map长度,避免重开内存浪费性能。

package main

import "fmt"

func main() {
	// nil map是不能赋值的
	var m0 map[string]string
	// m0["安徽"] = "合肥" // panic: assignment to entry in nil map
	fmt.Printf("m0 --> type:%T\tvalue:%v\t\tis_nil:%v\n", m0, m0, m0 == nil)

	// 声明式创建map
	var m1 = map[string]string{}
	m1["安徽"] = "合肥"
	fmt.Printf("m1 --> type:%T\tvalue:%v\tis_nil:%v\n", m1, m1, m1 == nil)
	// 使用make构造
	m2 := make(map[string]string, 10)
	m2["江苏"] = "南京"
	fmt.Printf("m2 --> type:%T\tvalue:%v\tis_nil:%v\n", m2, m2, m2 == nil)
}
e:\OneDrive\Projects\Go\src\gitee.com\studygo\day02\06-map>go run main.go
m0 --> type:map[string]string   value:map[]             is_nil:true
m1 --> type:map[string]string   value:map[安徽:合肥]    is_nil:false
m2 --> type:map[string]string   value:map[江苏:南京]    is_nil:false
package main

import "fmt"

func main() {
	errorCode := map[uint8]string{
		100: "argument is null",
		101: "connection timeout",
		102: "queue is full",
		103: "server refuse",
	}
	fmt.Printf("errorCode --> type:%T\tlen:%d\n", errorCode, len(errorCode))
	var status uint8 = 101
	fmt.Printf("ERROR:%d|%v", status, errorCode[status])
}
e:\OneDrive\Projects\Go\src\gitee.com\studygo\day02\06-map>go run main.go
errorCode --> type:map[uint8]string     len:4
ERROR:101|connection timeout

3.2. map的操作

3.2.1. 取值

通过 m[key]  可以取到key在map中对应的value,当key不存在时返回value的类型零值。
采用 value, ok := m[key] 获取的ok为bool值,ok为true时表示key在map,为false则不在。

package main

import "fmt"

func main() {
	errorCode := map[int]string{
		100: "argument is null",
		101: "connection timeout",
		102: "queue is full",
		103: "server refuse",
	}
	// 通过key取value
	var value string
	value = errorCode[101]
	fmt.Println(value)
	value = errorCode[104] // 不存在返回value类型的默认值,如string:"",bool:false
	fmt.Println(value, len(value))
	// 判断给定的值是否在 map 中
	status := 105
	v, ok := errorCode[status] // 第二个变量接收的值为bool类型
	if ok {
		fmt.Printf("%v 在errorCode中,其值为: %v\n", status, v)
	} else {
		fmt.Printf("%v 不在errorCode中\n", status)
	}
}
e:\OneDrive\Projects\Go\src\gitee.com\studygo\day02\06-map>go run main.go
connection timeout
 0
105 不在errorCode中

3.2.2. 遍历

package main

import "fmt"

func main() {
	errorCode := map[int]string{
		100: "argument is null",
		101: "connection timeout",
		102: "queue is full",
		103: "server refuse",
	}
	// 取key和value
	for key, value := range errorCode {
		fmt.Printf("%v:%v\t", key, value)
	}
	fmt.Printf("\n--------------------------------\n")
	// 取key
	for key := range errorCode {
		fmt.Printf("%v\t", key)
	}
	fmt.Printf("\n--------------------------------\n")
	// 取value
	for _, value := range errorCode {
		fmt.Printf("%v\t", value)
	}
}
e:\OneDrive\Projects\Go\src\gitee.com\studygo\day02\06-map>go run main.go
100:argument is null    101:connection timeout  102:queue is full       103:server refuse
--------------------------------
101     102     103     100
--------------------------------
argument is null        connection timeout      queue is full   server refuse

3.2.2. 修改

package main

import "fmt"

func main() {
	errorCode := map[int]string{
		100: "argument is null",
		101: "connection timeout",
	}
	// 插入
	errorCode[102] = "queue is full"
	errorCode[103] = "server refuse"
	fmt.Println(errorCode)
	// 更新
	errorCode[103] = "port 8080 already used"
	fmt.Println(errorCode)
}
e:\OneDrive\Projects\Go\src\gitee.com\studygo\day02\06-map>go run main.go
map[100:argument is null 101:connection timeout 102:queue is full 103:server refuse]
map[100:argument is null 101:connection timeout 102:queue is full 103:port 8080 already used]
package main

import "fmt"

func modify(m map[int]string) {
	m[103] = "server stopped"
}

func main() {
	errorCode := map[int]string{
		100: "argument is null",
		101: "connection timeout",
		102: "queue is full",
		103: "server refuse",
	}
	fmt.Println(errorCode)
	modify(errorCode)      // 引用类型
	fmt.Println(errorCode)
}
[root@heyingsheng 06-map]# go run update.go
map[100:argument is null 101:connection timeout 102:queue is full 103:server refuse]
map[100:argument is null 101:connection timeout 102:queue is full 103:server stopped]

3.2.3. 删除

package main

import "fmt"

func main() {
	errorCode := map[int]string{
		100: "argument is null",
		101: "connection timeout",
		102: "queue is full",
		103: "server refuse",
	}
	// 删除map中的元素
	delete(errorCode, 100) // 删除key/value
	delete(errorCode, 101) // 删除key/value
	delete(errorCode, 102) // 删除key/value
	delete(errorCode, 111) // 删除不存在的Key不会报错
	fmt.Println(errorCode)
}
e:\OneDrive\Projects\Go\src\gitee.com\studygo\day02\06-map>go run main.go
map[103:server refuse]

3.2.4. 清空map

清空一个map有两种方式,一种通过遍历并采用delete()删除 所有的元素。第二种直接将这个map重新用make分配新的内存空间,原有的内容将被GC自动回收。

package main

import (
	"fmt"
	"strings"
)

func main()  {
	m0 := make(map[string]string)
	m0["安徽"] = "合肥"
	m0["江苏"] = "南京"
	m0["浙江"] = "杭州"
	for k,_ := range m0 {
		delete(m0, k)
	}
	fmt.Println(m0)
	fmt.Println(strings.Repeat("---",10))
	m0["安徽"] = "合肥"
	m0["江苏"] = "南京"
	m0["浙江"] = "杭州"
	m0 = make(map[string]string)
	fmt.Println(m0)
}
[root@heyingsheng 06-map]# go run clear.go
map[]
------------------------------
map[]

3.3. 切片和map混合类型

3.3.1. 元素为map的切片

重点在于slice和map必须要指定长度,即必须要是非 nil 型

package main

import "fmt"

func main() {
	// 错误代码: panic: runtime error: index out of range [0] with length 0
	// slice 没有初始化,即长度为0,无法纳入数据
	// var s0 []map[int]string
	// s0[0][100] = "argument is null"
	// s0[0][101] = "connection timeout"
	// for _, value := range s0 {
	// 	fmt.Println(value)
	// }

	// 错误代码: panic: assignment to entry in nil map
	// 虽然slice初始化了,但是map没有
	// s0 := make([]map[int]string, 1)
	// s0[0][100] = "argument is null"
	// s0[0][101] = "connection timeout"
	// for _, value := range s0 {
	// 	fmt.Println(value)
	// }

	s0 := make([]map[int]string, 1)
	s0[0] = make(map[int]string, 10)
	s0[0][100] = "argument is null"
	s0[0][101] = "connection timeout"
	for _, value := range s0 {
		fmt.Println(value)  // map[100:argument is null 101:connection timeout]
	}
}

3.3.2. 值为切片的map

package main

import "fmt"

func main() {
	m0 := make(map[int][]string, 5)
	if _, ok := m0[100]; !ok {
		value := make([]string, 0, 2)
		value = append(value, "arg type error")
		value = append(value, "arg too many")
		m0[100] = value
	}
	fmt.Printf("%#v", m0[100]) // map[100:[arg type error arg too many]]
}

3.4. 案例

3.4.1. 统计英文单词数量

package main

import (
	"fmt"
	"strings"
)

func main() {
	var str = `where there is a will there is a way`
	// 统计s0中各个单词出现的次数
	s0 := strings.Split(str, " ") //如果涉及符号,可以考虑使用 strings.Replace去除特殊符号
    m0 := make(map[string]int8, len(s))
	for _, value := range s0 {
		m0[value]++ // 因为Int8的默认值为0,所以可以直接加
		// if _, ok := m0[value]; !ok {
		// 	m0[value] = 1
		// } else {
		// 	m0[value]++
		// }
	}
	fmt.Printf("%#v\n", m0)
}
e:\OneDrive\Projects\Go\src\gitee.com\studygo\day02\08-ex>go run main.go
map[string]int8{"a":2, "is":2, "there":2, "way":1, "where":1, "will":1}

3.4.2. map的排序

map本身是乱序的,也没有提供对map进行排序的方法,因此在对map排序时,需要先取出key放到切片中,然后对Key排序,再通过排序后的Key取出对应value.

package main

import (
	"fmt"
	"sort"
	"strings"
)

func main()  {
	m0 := make(map[int]string)
	m0[10204] = "合肥"
	m0[34572] = "南京"
	m0[99204] = "杭州"
	m0[55214] = "郑州"
	m0[23411] = "贵州"
	m0[77777] = "重庆"
	for k,v := range m0 {
		fmt.Printf("m0[%v]=%v  ", k, v)
	}
	fmt.Printf("\n%s\n", strings.Repeat("---",10))
	// 排序
	s0 := make([]int,0)
	for k, _ := range m0 {
		s0 = append(s0, k)
	}
	sort.Ints(s0)
	for _, k := range s0 {
		fmt.Printf("m0[%v]=%v  ", k, m0[k])
	}
	fmt.Println()
}
[root@heyingsheng 06-map]# go run sort.go
m0[34572]=南京  m0[99204]=杭州  m0[55214]=郑州  m0[23411]=贵州  m0[77777]=重庆  m0[10204]=合肥
------------------------------
m0[10204]=合肥  m0[23411]=贵州  m0[34572]=南京  m0[55214]=郑州  m0[77777]=重庆  m0[99204]=杭州