搞清楚golang数组、切片slice及映射map
很多从php转到golang的开发者,一直搞不清楚golang中的数组、slice切片、map映射,本文一次性从基础知识帮大家搞清楚他们。
目录
数组
-
数组
golang的数组不同于php。php数组的长度是动态的,数组赋值、传参默认都是引用赋值、传参,不是另开辟内存空间。
在声明定义的时候,长度就固定了,而且值类型必须一致。数组是值类型,不是引用类型,就是说赋值和传参都是复制整个数组另开内存空间。
元素item使用{}包起来
var a [4]int = [4]int{1,2,3} //不足4个元素,用int默认值0补足 var b = [...]int{1,2,3,4} //[...]表示初始化时确定长度 var c = [...]int{0:1,1:2,2:3} //支持索引 var dd = [...][2]int{{1,2,3},{3,2,1}} //多维数组 var arr1 [2][3]int = [...][3]int{{1, 2, 3}, {7, 8, 9}} //第二维不能使用... var d = [...]struct { //数组元素是结构体 name string age uint8 }{ {"user1", 10}, // 可省略元素类型。 {"user2", 20}, // 别忘了最后一行的逗号。 }
注意:数组是采用值拷贝,而值拷贝行为会造成性能问题,通常会建议使用 slice,或数组指针。
-
数组传参
前面说数组是值类型的,传参时默认也是值类型传参;
我们通常通过&来实现数组引用传参(除非一定要求值类型传参):
package main import "fmt" func sumArr(a [5]int) int { var sum int = 0 for i := 0; i < len(a); i++ { sum += a[i] } return sum } func printArr(arr *[5]int) { arr[0] = 10 for i, v := range arr { fmt.Println(i, v) } } func main() { var arr1 [5]int sum := sumArr(arr1) //值类型 printArr(&arr1) //引用类型,传址 fmt.Println(arr1) arr2 := [...]int{2, 4, 6, 8, 10} printArr(&arr2) fmt.Println(arr2) }
注意:& 和 *,可以解释成传址和取值(根据地址取值)。在c、php也通过&和*实现取址与取值。
是不是觉得golang的数组也太麻烦了,php的数组简单,正是因为php的解释型和动态性,使得php数组强大且编码简单,在性能上比不上golang在编译阶段就确定类型和长度的数组执行效率。
指针
前面数组中提到取址与取值,我们就先了解下golang的指针。
指针,从学编程开始,很多地方都在强调指针多重要,也不好理解,有常用到指针的语言通常都是比较难以精通的语言
指针是什么?指针就是内存地址。
在golang中,不能进行指针偏移和运算,属于安全性指针
两个符号:
- & :
变量取址
- * :
指针取值
三个概念:指针地址、指针类型、指针取值
- 指针地址:指针,变量的内存地址
- 指针类型:每个变量类型都对应一个指针类型,比如*string,*[4]int
- 指针取值:通过内存地址取得地址存储的值(也是变量的值,因为变量的内存地址就是指针)
func main() {
//指针取值
a := 10
b := &a // 取变量a的地址,将指针保存到b中,也就是变量b指向变量a指向的内存地址
fmt.Printf("type of b:%T\n", b)
c := *b // 指针取值(根据指针去内存取值)
fmt.Printf("type of c:%T\n", c)
fmt.Printf("value of c:%v\n", c)
}
输出:
type of b:*int
type of c:int
value of c:10
Slice切片
-
slice切片
slice表面上是数组的子集,但slice其实是
引用类型
,通过内部指针和相关属性引用数组片段实现slice的语法和python的列表切片语法是类似的
var a = [10]int{1,2,3,4,5} var slice1 = a[1:3] var slice2 = [:]
slice同样适合使用len、cap等数组可使用的函数方法,但受限于数组,比如长度不可能超出原数组
更多数组切片操作:
s[n] 切片s中的索引位置为n的元素 s[:] s[low:] s[:high] s[low:high] s[low:high:max] 从切片s的索引位置low到high获得切片,len=high-low,cap=max-low len(s) 切片长度 cap(s) 切片容量
data := [...]int{0, 1, 2, 3, 4, 5} s := data[2:4] s[0] += 100 s[1] += 200 fmt.Println(s) fmt.Println(data)
输出:
[102 203] [0 1 102 203 4 5]
注意:切片采用的是
左闭右开
方式,即包含索引low但不包含索引high的元素,也是大家说的前包后不包;切片赋值默认是引用类型,所以更改了切片元素时,也会更改原数组的元素值参考切片内存地址:
回过头来看php的数组,对开发者太友好了,不需要记忆太多语法糖,学习成本低,看起来符合自然易理解,不用关心固定不固定长度,开发者想怎么处理数组就怎么处理,不用担心是不是会出错,出错的概率太低了,php的数组兼容性太强,表达出了简约不简单的理念。
-
make直接创建切片
make是用于内存分配的,区别于new,它只用于slice、map以及chan的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了
make函数是无可替代的,我们在使用slice、map以及channel的时候,都需要使用make进行初始化,然后才可以对它们进行操作。
make可以创建切片
var slice []type = make([]type, len, cap)
slice := make([]int,0,5) //创建切片:类型int、初始长度0、容量5
除了make可以创建切片外,还可以通过初始化表达式构造:
var a int[] = []int{1,2,3,4,5}
直接创建切片,底层会自动创建数组。也就是说切片都是基于数组基础上
-
二维切片
data := [][]int{ []int{1, 2, 3}, []int{100, 200}, []int{11, 22, 33, 44}, } fmt.Println(data)
[[1 2 3] [100 200] [11 22 33 44]]
php开发者看到这里估计有点不耐烦了,为什么这么麻烦呢,哈哈
//php一个动态数组就这样定义,只因为类型不要求,甚至可以类型混合在一起,还支持键值对。确实php给开发者的心里负担少了很多,不用关心太多类型、长度等问题,只要关心业务逻辑问题,当然php赢了空间输了时间。 $data = [ [1,2,3], [100,200], [11,22,33,44], ["a",'b',3], ["a"=>"apple",2] ]; var_dump($data);
-
append
切片追加
var a = []int{1, 2, 3} fmt.Printf("slice a : %v\n", a) var b = []int{4, 5, 6} fmt.Printf("slice b : %v\n", b) c := append(a, b...) fmt.Printf("slice c : %v\n", c) d := append(c, 7) fmt.Printf("slice d : %v\n", d) e := append(d, 8, 9, 10) fmt.Printf("slice e : %v\n", e)
注意:如果append后超出原切片的cap容量,将复制数据并分配一个新数组(重新分配地址,即切片与原数组已经不再引用同一个地址)
-
copy
切片复制,函数 copy 在两个 slice 间复制数据,复制长度以 len 小的为准。两个 slice 可指向同一底层数组,允许元素区间重叠。
-
遍历
data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} slice := data[:] for index, value := range slice { fmt.Printf("inde : %v , value : %v\n", index, value) }
-
字符串与切片
string在底层是一个byte的数组,也支持切片操作
对于静态编译语言,string定义后是不可变的,要修改字符,需要转换成[]byte(str)或[]rune(str)处理后,再强制转换成string。rune用于处理中文字符串,前面已经提及
对于php动态脚本语言,string变量运行时是动态可变的,牺牲性能与空间来不麻烦php开发人员
map映射
map是一种无序的基于key-value的数据结构,Go语言中的map是引用类型,必须初始化才能使用
-
定义map
语法:make(map[KeyType]ValueType, [cap])
func main() { scoreMap := make(map[string]int, 8){ "小红":99 } scoreMap["张三"] = 90 scoreMap["小明"] = 100 fmt.Println(scoreMap) fmt.Println(scoreMap["小明"]) fmt.Printf("type of a:%T\n", scoreMap) }
看了这个示例,有php开发者可能又要说php数组了,php数组可以是列表、对象、结构体,动态混合可调整,还能互相转换,不考虑编译/解释、空间效率问题,让php开发者会觉得很轻松。强类型语言就是要求变量定义的时候尽量明确类型、大小,因为在编译的时候,尽量要知道变量的类型及要分配的空间大小,减少运行时的检查与转换,提高运行时的效率。
-
判断键值对是否存在
func main() { scoreMap := make(map[string]int) scoreMap["小红"] = 90 scoreMap["小明"] = 100 // 如果key存在ok为true,v为对应的值;不存在ok为false,v为值类型的零值 v, ok := scoreMap["小红"] if ok { fmt.Println(v) } else { fmt.Println("找不到") } }
-
遍历
采用迭代器range进行遍历,map遍历是无序的
-
删除
delete(map,key)