目录
在 上一篇文章 中讲述了Go语言的基本数据类型:整型、浮点型、字符串的用法,除了基本数据类型以外,Go语言中还有复合数据类型,由多个相同类型或不同类型元素的值组合而成。Go 语言内置了如下的复合数据类型:数组(array)、切片(slice)、映射(map)、结构体(struct)、channel等。
【array】
Go 的数组类型包含两个属性:元素的类型 和 元素的个数(也就是数组长度),元素的类型可以是任意的 Go 原生类型或自定义类型,数组长度必须在声明数组的时候提供。如果两个数组的元素类型 与 数组长度 都是一样的则是等价数组,如果有一个属性不同则是两个不同的数组类型。数组在内存中占据着一整块连续的内存,这块内存全部空间都被用来表示数组元素。如果两个数组所分配的内存大小不同则是不同的数组类型。
数组的基本用法
使用 len() 函数可以获取数组的长度,使用 unsafe.Sizeof() 函数可以获取数组的总大小。
var arr2 = [6]int{1, 2, 3, 4, 5, 6}
fmt.Println("数组长度:", len(arr2)) // 6
fmt.Println("数组大小:", unsafe.Sizeof(arr2)) // 48 //64位平台上int 类型的大小为8,因此是6*8=48
可以指定数组长度,也可以用 ... 代替
f1 := [5]int{1, 2, 3, 4, 5}
f1 := [...]int{1, 2, 3, 4, 5}
可以使用下标赋值的方式对数组初始化,跳过某些元素
g := [...]int{1: 3, 6: 5}
fmt.Println(g) //[0 3 0 0 0 0 5]
fmt.Println(len(g)) // 7
如果不显式初始化,那么数组中的元素值就是它类型的零值。比如下面的数组类型变量 arr1 的各个元素值都为 0
var arr1 [6]int
fmt.Println(arr1) // [0 0 0 0 0 0]
数组由零个或多个元素组成,一旦声明了,数组的长度就固定了,不能动态变化。数组的 长度len() 和 容量cap() 返回结果始终一样。
数组的更多用法
//go01/array.go
package main
import "fmt"
func main() {
arr := [3]int{1, 2, 3}
fmt.Println(arr)
//for range 遍历数组
for key, val := range arr {
fmt.Print(key, "==>", val, ",") //0==>1,1==>2,2==>3,
}
fmt.Println()
fmt.Println(arr[2]) //打印第三个数值:3
//fmt.Println(arr[4]) //invalid array index 4 (out of bounds for 4-element array)
fmt.Printf("长度:%d, 容量:%d \n", len(arr), cap(arr))
//因为数组定长,长度和容量相同
arr[0] = 100
fmt.Println(arr[0])
//字符串数组
var e = [5]string{"name", "age", "sex"}
fmt.Println(e)
f := [...]int{1, 2, 3, 4, 5}
fmt.Println(f)
g := [...]int{1: 3, 6: 5}
fmt.Println(g) //[0 3 0 0 0 0 5]
fmt.Println(len(g)) // 7
//数组分割
arr3 := [...]int{1, 2, 3, 4, 5}
arr3Sec := arr3[:3]
fmt.Println(arr3Sec) //[1 2 3]
//冒泡排序
array := [5]int{15, 23, 8, 10, 7}
for i := 1; i < len(array); i++ {
for j := 0; j < len(array)-i; j++ {
if array[j] > array[j+1] {
array[j], array[j+1] = array[j+1], array[j]
}
}
}
fmt.Println(array) //[7 8 10 15 23]
//二维数组
a := [3][4]int{{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}}
fmt.Println(a)
fmt.Printf("二维数组的地址:%p,长度:%d \n", &a, len(a))
}
【slice】
数组在使用上有两点不足:(1)固定的元素个数,(2)数组是值传递,会导致开销较大。
因此,Go引入了切片Slice。Go 语言的切片类型属于引用类型,同属引用类型的还有字典类型、通道类型、函数类型等;而 Go 语言的数组类型则属于值类型,同属值类型的有基础数据类型以及结构体类型。从传递成本的角度讲,引用类型的值往往要比值类型的值低很多。Go语言中切片的定义如下:
type slice struct {
array unsafe.Pointer //指向底层数组的指针
len int //切片的长度,即切片中当前元素的个数
cap int //底层数组的长度,也是切片的最大容量,cap 值永远大于等于 len 值
}
创建切片
切片和数组相比少了长度的定义。但切片也有自己的长度,只不过这个长度是随着切片中元素个数的变化而变化的。使用 len() 获取切片的长度,使用 cap() 获取切片的容量。
sli := []int{1, 2, 3, 4, 5, 6}
fmt.Printf("len=%d cap=%d slice=%v\n", len(sli), cap(sli), sli) //len=6 cap=6 slice=[1 2 3 4 5 6]
可以通过 make 函数来创建切片,并指定底层数组的长度
sli1 := make([]byte, 6, 10) // 其中10为cap值,即底层数组长度,6为切片的初始长度
fmt.Printf("sli1:%d, len:%d, cap:%d \n", sli1, len(sli1), cap(sli1)) //sli1:[0 0 0 0 0 0], len:6, cap:10
sli2 := make([]byte, 6) //如果没有在 make 中指定 cap 参数,那么底层数组长度 cap 就等于 len
fmt.Printf("sli2:%d, len:%d, cap:%d \n", sli2, len(sli2), cap(sli2)) //sli2:[0 0 0 0 0 0], len:6, cap:6
基于一个已存在的数组创建切片
//array[low : high : max]
arrx := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
slx3 := arrx[3:7:9]
fmt.Println(slx3) //[4 5 6 7]
切片只能和nil比较,不能两个切片对比
a := []int{1, 2, 3, 4}
b := []int{1, 2, 3, 4}
// fmt.Println(a == b) //切片只能和nil比较,所以此处会报错
fmt.Println(a, b)
切片的动态扩容
使用 append() 对切片扩容
//基于数组创建切片
//采用 array[low : high : max]语法基于一个已存在的数组创建切片
arrx := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
slx3 := arrx[3:7:9]
fmt.Println(slx3) //[4 5 6 7]
//数组和切片的关系变化
var arr = [8]int{1, 2, 3, 4, 5, 6, 7, 8}
var slice = arr[0:3]
fmt.Println(slice, &arr[0] == &slice[0]) //[1 2 3] true
//切片扩容
slice[1] = 22 //把切片的第1位改成22: [1 22 3]
slice = append(slice, 11) //给切片追加元素(也就是扩容)
slice = append(slice, 12)
slice = append(slice, 13)
slice = append(slice, 14)
fmt.Println(arr) //[1 22 3 11 12 13 14 8]
fmt.Printf("slice=%d,len=%d,cap=%d \n", slice, len(slice), cap(slice)) //slice=[1 22 3 11 12 13 14],len=7,cap=8
fmt.Println(&slice[0] == &arr[1]) //false, 此时原来的arr和现在的slice已经不是一个内存地址了
数组和切片都可以通过[start:end] 的形式来获取子切片,左闭右开:
1. arr[start:end],获得[start, end)之间的元素
2. arr[:end],获得[0, end) 之间的元素
3. arr[start:],获得[start, len(arr))之间的元素
使用 for 语句遍历切片
//遍历切片
for i := 0; i < len(slice); i++ {
fmt.Print(slice[i], " ") //1 22 3 11 12 13 14
}
fmt.Println()
for i, v := range slice {
fmt.Printf("%d => %d\n", i, v)
}
/*
0 => 1
1 => 22
2 => 3
3 => 11
4 => 12
5 => 13
6 => 14
*/
分割切片(子切片)
sli := []int{1, 2, 3, 4, 5, 6}
fmt.Printf("len=%d cap=%d slice=%v\n", len(sli), cap(sli), sli) //len=6 cap=6 slice=[1 2 3 4 5 6]
fmt.Println("sli[1] ==", sli[1]) //sli[1] == 2
fmt.Println("sli[:] ==", sli[:]) //sli[:] == [1 2 3 4 5 6]
fmt.Println("sli[1:] ==", sli[1:]) //sli[1:] == [2 3 4 5 6]
fmt.Println("sli[:4] ==", sli[:4]) //sli[:4] == [1 2 3 4]
fmt.Println("sli[0:3] ==", sli[0:3]) //sli[0:3] == [1 2 3]
fmt.Printf("len=%d cap=%d slice=%v\n", len(sli[0:3]), cap(sli[0:3]), sli[0:3]) //len=3 cap=6 slice=[1 2 3]
fmt.Println("sli[0:3:4] ==", sli[0:3:4]) //sli[0:3:4] == [1 2 3]
fmt.Printf("len=%d cap=%d slice=%v\n", len(sli[0:3:4]), cap(sli[0:3:4]), sli[0:3:4]) //len=3 cap=4 slice=[1 2 3]
删除切片元素
sli = []int{1, 2, 3, 4, 5, 6, 7, 8}
fmt.Printf("len=%d cap=%d slice=%v\n", len(sli), cap(sli), sli) //len=8 cap=8 slice=[1 2 3 4 5 6 7 8]
//删除尾部 2 个元素
fmt.Printf("len=%d cap=%d slice=%v\n", len(sli[:len(sli)-2]), cap(sli[:len(sli)-2]), sli[:len(sli)-2]) //len=6 cap=8 slice=[1 2 3 4 5 6]
//删除开头 2 个元素
fmt.Printf("len=%d cap=%d slice=%v\n", len(sli[2:]), cap(sli[2:]), sli[2:]) //len=6 cap=6 slice=[3 4 5 6 7 8]
//删除中间 2 个元素
sli = append(sli[:3], sli[3+2:]...)
fmt.Printf("len=%d cap=%d slice=%v\n", len(sli), cap(sli), sli) //len=6 cap=8 slice=[1 2 3 6 7 8]
切⽚之间共享存储空间
year := []string{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep",
"Oct", "Nov", "Dec"}
Q2 := year[3:6]
fmt.Println(Q2, len(Q2), cap(Q2)) //[Apr May Jun] 3 9
summer := year[5:8]
fmt.Println(summer, len(summer), cap(summer)) //[Jun Jul Aug] 3 7
summer[0] = "Unknow" //这里修改子切片的值,原切片值也会被修改
fmt.Println(Q2) //[Apr May Unknow]
fmt.Println(year)
切片排序
//切片排序,可以使用冒泡
sl := []int{15, 23, 8, 10, 7}
for i := 1; i < len(sl); i++ {
for j := 0; j < len(sl)-i; j++ {
if sl[j] > sl[j+1] {
sl[j], sl[j+1] = sl[j+1], sl[j]
}
}
}
fmt.Println(sl) //[7 8 10 15 23]
//也可以直接使用sort包下的排序方法
sl2 := []int{19, 2, 8, 11, 30}
sort.Ints(sl2)
fmt.Println(sl2) //[2 8 11 19 30]
//字符串切片排序
sl3 := []string{"Apple", "Windows", "Orange", "abc", "你好", "acd", "acc"}
sort.Strings(sl3)
fmt.Println(sl3) //[Apple Orange Windows abc acc acd 你好]
切片总结:切片是一个引用类型的容器,底层指向的是一个数组,切片是对数组的连续片段引用。切片可以是整个数组,也可以是数组的一部分,保存有数组开始的下标、长度、容量;注意切片的 len() 和 cap() 返回的结果不一定相同。使用 append() 方法向切片的尾部追加元素。
- 每一个切片引用了一个底层数组
- 切片本身不存储任何数据,都是这个底层数组存储,所以修改切片也就是修改这个数组中的数据
- 当向切片中添加数据时,如果没有超过容量,直接添加,如果超过容量,自动扩容(成倍增长)
- 切片一旦扩容,就是重新指向一个新的底层数组
【map】
map 一般被翻译为映射、哈希表或字典。Go语言中的map表示一组无序的键值对,也就是key/value结构,并且每个key都是唯一的,key 与 value 的类型可以相同也可以不同。如果两个 map 的 key 元素类型相同,value 元素类型也相同,那么它们就是同一个 map 类型,否则就是不同的 map 类型。
在 Go 语言中,函数类型、map 类型自身,以及切片只支持与 nil 的比较,而不支持同类型两个变量的比较。 map的 key 不能使用 函数类型、map 类型、切片类型,为什么?
因为Go语言规定了,键类型的值必须要支持 == 和 != 操作,由于函数、map、切片 的值并不支持判断等于或不等于的操作,所以map的键类型不能是这三种类型。另外,如果键的类型是接口类型的,那么键值的实际类型也不能是上述三种类型,否则在程序运行过程中会引发 panic
s1 := make([]int, 1)
s2 := make([]int, 2)
f1 := func() {}
f2 := func() {}
t1 := make(map[int]string)
t2 := make(map[int]string)
//println(s1 == s2) // 错误:invalid operation: s1 == s2 (slice can only be compared to nil)
//println(f1 == f2) // 错误:invalid operation: f1 == f2 (func can only be compared to nil)
//println(t1 == t2) // 错误:invalid operation: t1 == t2 (map can only be compared to nil)
//key作为接口类型时,也不能使用 函数、map、切片
var badMap = map[interface{}]int{
"1": 1,
[]int{2}: 2, //panic: runtime error: hash of unhashable type []int
3: 3,
}
//如果把上面的 []int{2}: 2 改成 [1]int{2}: 2,即把切片类型改成数组类型就没问题了
var badMap = map[interface{}]int{
"1": 1,
[1]int{2}: 2, //panic: runtime error: hash of unhashable type []int
3: 3,
}
fmt.Println(badMap) //map[3:3 1:1 [2]:2]
创建map
注意:对零值状态的 map 变量直接进行操作,就会导致运行时异常(panic)
//创建map
var map1 map[int]string //没有初始化,值为 nil
// map1[5] = "test" //报错:panic: assignment to entry in nil map(因为未初始化)
var map2 = make(map[int]string) //初始化
// map2[5] = "test" //不会报错
var map3 = map[string]int{"Go": 98, "Python": 87, "Java": 79, "Html": 93}
fmt.Println(map1) //map[]
fmt.Println(map2) //map[]
fmt.Println(map3) //map[Go:98 Html:93 Java:79 Python:87]
fmt.Println(map1 == nil) //true
fmt.Println(map2 == nil) //false
fmt.Println(map3 == nil) //false
//如果没有初始化就赋值,会报错 panic: assignment to entry in nil map
//因此可以这样判断
if map1 == nil {
map1 = make(map[int]string)
fmt.Println(map1 == nil) //false
}
//或者使用短变量
mx := map[string]int{}
mx["key"] = 1
fmt.Println(mx) //map[key:1]
//初始化并指定容量
map4 := make(map[int]string, 8) // 指定初始容量为8
//稍微复杂一点的map初始化
type Position struct {
x float64
y float64
}
m2 := map[Position]string{
Position{29.935523, 52.568915}: "school",
Position{25.352594, 113.304361}: "shopping-mall",
Position{73.224455, 111.804306}: "hospital",
}
fmt.Println("m2", m2) //map[{25.352594 113.304361}:shopping-mall {29.935523 52.568915}:school {73.224455 111.804306}:hospital]
//上面的初始化方式可以简化一下:Go 允许省略字面值中的元素类型
m3 := map[Position]string{
{29.935523, 52.568915}: "school",
{25.352594, 113.304361}: "shopping-mall",
{73.224455, 111.804306}: "hospital",
}
fmt.Println("m3", m3) //map[{25.352594 113.304361}:shopping-mall {29.935523 52.568915}:school {73.224455 111.804306}:hospital]
map的增删改查
//存储键值对到map中
map1[1] = "语文"
map1[2] = "数学"
map1[3] = "英语"
map1[4] = "物理"
map1[7] = ""
fmt.Println(map1) //map[1:语文 2:数学 3:英语 4:物理 7:]
//使用 for...range... 遍历map,打印结果无序
for k, v := range map1 {
fmt.Print(k, "=>", v, " ") //1=>语文 2=>数学 3=>英语 4=>物理 7=>
}
fmt.Println()
//将map的key组成新的slice
keys := make([]int, 0, len(map1))
for k, _ := range map1 {
keys = append(keys, k)
}
fmt.Println(keys) //[1 2 3 4 7]
//根据key获取对应的value值
fmt.Println(map1[4]) //物理
fmt.Println(map1[40]) //""
//在访问的 Key 不存在时,仍会返回零值,因此不能通过返回 nil 来判断元素是否存在
v1, ok := map1[40]
if ok {
fmt.Println("对应的数值是:", v1)
} else {
fmt.Println("操作的key不存在")
}
//如果并不关心某个键对应的 value,而只关心某个键是否在于 map 中,可以使用空标识符替代变量 v
_, ok = map1[40]
fmt.Println(ok) //false
//修改map的数据
map1[3] = "历史"
fmt.Println(map1) //map[1:语文 2:数学 3:历史 4:物理 7:]
//删除map的数据
delete(map1, 3)
fmt.Println(map1) //map[1:语文 2:数学 4:物理 7:]
//获取map的长度
fmt.Println(len(map1)) //4
//获取map的类型
map11 := make(map[int]string)
map12 := make(map[string]float64)
fmt.Printf("%T\n", map11) //map[int]string
fmt.Printf("%T\n", map12) //map[string]float64
map只有长度len,没有容量cap
注意:如果使用的map的key是int类型,建议优先使用slice,效率会比较高,内存占用比较少
map和切片都是引用类型, 因此当 map 类型变量作为参数被传递给函数或方法的时候,在函数内部对 map 的修改会影响函数外部的值。
因为 map 可以自动扩容,map 中数据元素的 value 位置可能在这一过程中发生变化,所以 Go 不允许获取 map 中 value 的地址
func main() {
//map是引用传递
m := make(map[int]string)
m[1] = "value1"
m[3] = "value2"
foo(m)
fmt.Println(m) //map[1:changed 3:value2]
//不允许获取 map 中 value 的地址
p := &m[1] //invalid operation: cannot take address of m[1] (map index expression of type string)
}
func foo(data map[int]string) {
data[1] = "changed"
}
map的并发问题
map 不是并发写安全的,不支持同时并发读写。如果对 map 实例进行并发读写,程序运行时就会抛出异常。
func main() {
m := map[int]int{
1: 11,
2: 12,
3: 13,
}
go func() {
for i := 0; i < 1000; i++ {
doIteration(m)
}
}()
go func() {
for i := 0; i < 1000; i++ {
doWrite(m)
}
}()
time.Sleep(5 * time.Second)
//执行后报错:fatal error: concurrent map iteration and map write
//把上面的 doIteration() 和 doWrite() 任意一个注释掉就OK了
}
func doIteration(m map[int]int) {
for k, v := range m {
_ = fmt.Sprintf("[%d, %d] ", k, v)
}
}
func doWrite(m map[int]int) {
for k, v := range m {
m[k] = v + 1
}
}
map 如果只是并发读或只是并发写是没有问题的。另外,Go 1.9 版本中引入了支持并发写安全的 sync.Map 类型,可以用来在并发读写的场景下替换掉 map。关于并发相关的知识点后面再细说。
使用map实现工厂模式
map 的 value 可以是一个普通的类型,也可以是⼀个⽅法。利用这一特性可以⽅便的实现单⼀⽅法对象的⼯⼚模式。(关于方法后面再讲)
m := map[int]func(op int) int{}
m[1] = func(op int) int { return op } //返回原值
m[2] = func(op int) int { return op * op } //计算平方
m[3] = func(op int) int { return op * op * op } //计算立方
fmt.Println(m[1](2)) //2
fmt.Println(m[2](2)) //4
fmt.Println(m[3](2)) //8
使用map实现集合
redis中有个“集合”的类型,可以存储无序的数据,无序集合的特点: 唯一性,无序性,确定性。具体使用方法参考:redis笔记04-无序集合和有序集合_redis 有序集合,无序集合_浮尘笔记的博客-CSDN博客
如何在Go中实现一个类似于redis的集合?可以使用map,将map的值设为bool类型,代码如下:
findNum := 1
mySet := map[int]bool{}
mySet[1] = true //添加元素
mySet[3] = true //添加元素
fmt.Println(mySet[findNum]) //判断元素是否存在:true
fmt.Println(len(mySet)) //获取元素个数
delete(mySet, 1) //删除元素
fmt.Println(mySet[findNum]) //false
使用map返回给前端json数据
/*多个map组合成为slice,模拟返回给客户端的接口列表*/
//创建多个map存储用户的信息
user1 := make(map[string]string)
user1["name"] = "张三"
user1["age"] = "30"
user1["sex"] = "男"
user2 := map[string]string{"name": "李四", "age": "28", "sex": "女"}
user3 := map[string]string{"name": "王五", "age": "21", "sex": "女"}
//将map存入到slice中
list := make([]map[string]string, 0, 3)
list = append(list, user1)
list = append(list, user2)
list = append(list, user3)
//遍历结果
for i, val := range list {
fmt.Printf("用户 %d:", i+1)
fmt.Printf("姓名:%s,", val["name"])
fmt.Printf("年龄:%s,", val["age"])
fmt.Printf("性别:%s\n", val["sex"])
}
//将结果输出为json
result, _ := json.Marshal(list)
fmt.Println(string(result)) //[{"age":"30","name":"张三","sex":"男"},{"age":"28","name":"李四","sex":"女"},{"age":"21","name":"王五","sex":"女"}]
/*二维结构*/
maps := make(map[string]map[string]string)
maps["user"] = map[string]string{"name": "zhangsan", "age": "30"}
maps["fruit"] = map[string]string{"name": "apple", "price": "6"}
res, _ := json.Marshal(maps)
fmt.Println(string(res)) //{"fruit":{"name":"apple","price":"6"},"user":{"age":"30","name":"zhangsan"}}
/*封装*/
res := make(map[string]interface{})
res["code"] = 200
res["msg"] = "success"
res["data"] = map[string]interface{}{
"username": "Tom",
"age": "30",
"hobby": []string{"读书", "爬山"},
}
fmt.Println("map data :", res) // map[code:200 data:map[age:30 hobby:[读书 爬山] username:Tom] msg:success]
//序列化
jsons, errs := json.Marshal(res)
if errs != nil {
fmt.Println("json marshal error:", errs)
}
fmt.Println("--- map to json ---")
fmt.Println("json data :", string(jsons)) // {"code":200,"data":{"age":"30","hobby":["读书","爬山"],"username":"Tom"},"msg":"success"}
//反序列化
res2 := make(map[string]interface{})
errs = json.Unmarshal([]byte(jsons), &res2)
if errs != nil {
fmt.Println("json marshal error:", errs)
}
fmt.Println("--- json to map ---")
fmt.Println("map data :", res2) // map[code:200 data:map[age:30 hobby:[读书 爬山] username:Tom] msg:success]
map的注意事项
- map和切片都是引用传递
- map没有cap()容量
- map 不是线程安全的,不支持并发读写
- map中的元素是无序的,每次遍历得到的顺序可能不一样
- map中新增的元素如果key已经存在,会覆盖已存在的key的值
扩展:关于深拷贝和浅拷贝
深拷贝:拷贝的是数据本身。值类型的数据默认都是深拷贝:array,int,float,string,bool,struct
浅拷贝:拷贝的是数据地址。导致多个变量指向同一块内存,引用类型的数据,默认都是浅拷贝:slice,map,因为切片是引用类型的数据,直接拷贝的是地址。使用copy() 可以实现切片的拷贝。
sla := []int{1, 2, 3, 4}
slb := []int{7, 8, 9}
copy(slb[1:], sla[2:]) //将sla中的部分元素(3,4)拷贝到slb中
fmt.Println(sla) //[1 2 3 4]
fmt.Println(slb) //[7 3 4]
本篇内容涉及的源代码请参考:go-demo-2023: Go语言基本用法和实用笔记 - Gitee.com
【路虽远,行则将至。加油!】