本文主要对比 PHP 中的 Array 和 Golang 中的 Array&Slice&Map,不做基础语法的介绍
本文概要
- 思维速转,PHP Array -> Go Array or Slice or Map
- 底层实现的异同
- 使用中的注意事项及坑坑
- 如何选择 Array 和 Slice
- 值传递和引用传递的坑
- Map 的无序
- 并发安全性
一、思维速转
PHP 中 Array 实在是太好用了,即可以声明为索引数组,又可以声明为关联数组;但是在 Golang 中,被拆解成了多个数据类型,其中索引数组与 Array/Slice 更贴近,而关联数组更贴近于 Map
// 索引数组
$colors = array("red", "green", "blue");
// Go Array: [3]string{"red", "green", "blue"} // 固定长度
// Go Slice: []string{"red", "green", "blue"}
// 关联数组
$person = array(
"first_name" => "John",
"last_name" => "Doe",
"age" => 30
);
// 对比 Golang Map
person := map[string]string{
"first_name": "John",
"last_name": "Doe",
"age": "30", // 特别注意强类型下 map 中值部分无法存放不同类型的数据
}
二、底层实现的异同
PHP 的语法上屏蔽了索引数组和关联数组,底层实现均基于 HashTable
完成,但是对于 Golang 来说,不同的类型实现是略有差异的;
Array 底层实现
Go 的 Array 一定是预先确定长度的,故底层分配的是固定连续内存,变量在声明后,直接指向数组存储的内存地址,所以读取会非常高效
arr := [3]int32{1,2,3}
fmt.Println(&arr[0],&arr[1],&arr[2])
// Output: 可以看到每个元素偏移刚好是 4 个字节
0xc0000a6030 0xc0000a6034 0xc0000a6038
特别注意,对于[3]string 字符串数组来说,string 实际上是存放在独立的内存中的,所以数组中保存的是字符串地址,所以打印出来的地址偏移是 16 个字节(string 底层数据结构包括一个指向字符串真实地址的指针,以及一个字符串长度的值)
注意,所有这些字节偏移均是 64 位机器上
Slice 底层实现
Slice 实际上底层也是引用了数组的实现,但是它在数组的上面又封装了一层数据结构,当我们打印 Slice 中每个元素的内存地址时你会发现其每个元素的偏移规则与数组是一致的。差别在于当我们打印整个数组的指针地址时会发现其与数组第一个元素的地址是一致的,而 Slice 的不同。
type slice struct {
array unsafe.Pointer // 指向底层数组
len int
cap int
}
arr := [3]int64{1,2,3}
fmt.Printf("arr : %p \n", &arr)
fmt.Println(&arr,&arr[0],&arr[1],&arr[2])
slice := []int64{1,2,3}
fmt.Printf("slice : %p \n", &slice)
fmt.Println(&slice,&slice[0],&slice[1],&slice[2])
// Output:
arr : 0xc000018120 // 与第一个元素一致
&[1 2 3] 0xc000018120 0xc000018128 0xc000018130
slice : 0xc000010078 // 指向 Slice 结构体指针,故与第一个元素内存不一致
&[1 2 3] 0xc000018150 0xc000018158 0xc000018160
Slice 实际上实现了高效的动态扩容能力,故对比 Array 来说应用场景更广泛
Map 底层实现
Golang 的 Map 底层实现也同样是 HashTable
,相同点是,查找插入效率高,支持动态扩容等,但是 Golang 下的 Map 在遍历时是无序的,php 下的关联数组则是有序的;
关于 Golang 中 Array,Slice,Map 更详细的底层实现有大量文章分析,这里不再赘述
使用中的注意事项及坑坑
Array 和 Slice 的选择
Array 在某些场景下的确是使用起来会更高效,所以原则上在某些特别注意性能,且的确是不需要数据长度变动的情况下可以选择 Array,但是,我要特别说明,这种场景极少极少,在日常业务系统开发中,无脑选择使用 Slice 通常是正确的选择
值传递和引用传递
Array 是值传递,Slice 和 Map 是引用传递,换句话说,Array 将数据传递到子函数后在子函数做任何修改,不会影响父函数内原始数据,但是 Slice 和 Map 不一样,子函数对数据的修改会修改原始数据,所以在上面无脑选择 Slice 时要特别注意这一项,是否担心子函数意外修改数据
Map 的无序的坑
这个从 PHP 转到 Go 可能无法注意到的一点,每次循环 Map 居然可能会得到完全不一样的顺序的结果,所以要想保证 Map 顺序通常需要配合 slice 完成,或者借助第三方库,比如:https://github.com/iancoleman/orderedmap
和 https://github.com/wangjia184/sortedset
但实际上这种可能也会带来 json 序列化的坑,所以从接口设计上就应尽可能避免,对有序要求的尽可能使用 Slice
并发安全性
Go 是一个语言级别就支持并发的语言,但是 Slice 和 Map 都是并发不安全的类型,所以在并发操作 Slice 和 Map 时可能会引起冲突,故要特别注意,修改数据时需要引入 sync.Mutex 互斥锁 or 利用 channel 完成,Map 还提供了一个并发安全的类型 sync.Map 来解决这个问题。
强类型下 Golang Array灵活性降低
接受它!
当你接触了 Golang 下
数组们
的实现,会发现它真的没有 php 下的数组灵活好用,这其实是强类型语言的通病,虽然 Golang 也可以通过定义 []interface{} 类型的 Slice,但是强烈不建议这样用,强类型的优点是让我们尽可能早的发现问题,比如在编码时 IDE 就能做到提示,而非在运行
下一章,讲下 PHP 中类和函数的映射,这里面会涉及到 Golang 的接口、函数和结构体