接触go已经快两年了, 老是想把学过的东西写下来, 但是一直没能行动, 今天算开个头吧.
基础数据类型
在golang里有int(int、 int8、 int16、 int32、 int64、 uint、 uint8、 uint16、 uint32、 uint64)、 float(float32、 float64)、 struct、 array、 slice、 map、 interface{}等。 他们的介绍基本都知道就不说了, 我写点比较冷门的。
int和uint
int或者uint的长度是多少? 32位? 64位? 都有可能。 在golang中int的系统平台决定, 当系统是32位时(也就是GOARCH=x86)int或uint的长度为32位, 当系统为64位时(也就是GOARCH=amd64)int或uint的长度为64位。 所以int或uint的长度不是固定的, 而是由系统架构决定。 还有种说法是int的长度由计算机的cpu字长决定, 这种说明我认为不严谨, 在64位的系统上是可以运行32位的Go程序的, 在32位Go程序中int的长度肯定是32位。
传递类型
调用函数传递参数有两种情况,他们分别是:
- 值传递
- 指针传递
指针传递传递的是一个指针, 也就是原函数中变量在内存中的位置, 通过修改该位置内存上的内容会导致原函数中变量的内容也会改变。而值传递是在调用函数上创建一个和原函数参数变量一样的变量,再将原函数变量内容拷贝到调用函数新创建的变量中,调用函数中的变量与原函数变量值就是两个独立的变量互不影响。下面举例说明一下:
//值传递
package main
import "fmt"
func call(a int) {
a++
fmt.Println(a) //2
}
func main() {
var a int = 1
call(a)
fmt.Println(a) //1
}
//指针传递
package main
import "fmt"
func call(a *int) {
*a++
fmt.Println(*a) //2
}
func main() {
var a int = 1
call(&a)
fmt.Println(a) //2
}
在go语言中map,slice,chan是指针传递而array、struct和其他一些基础数据类型(int,float等)下面我们来探究下slice。slice和array差不多,但是slice有array所不具备的特性,比如扩容等。下面我们先来看一个例子。
package main
import "fmt"
func arrayCall(a [3]int) {
fmt.Println(a) //[1,2,3]
a[0] = 3
a[1] = 2
a[2] = 1
fmt.Println(a) //[3,2,1]
}
func sliceCall(s []int) {
fmt.Println(len(s), cap(s)) // 3,5
s[0] = 3
s[1] = 2
s[2] = 1
fmt.Println(s) //[3,2,1]
s = append(s, 4, 5)
fmt.Println(len(s), cap(s)) // 5,5
fmt.Println(s) //[3,2,1,4,5]
s = append(s, 6)
fmt.Println(len(s), cap(s)) // 6,10
fmt.Println(s) //[3,2,1,4,5,6]
s[0] = 0
fmt.Println(s) //[0,2,1,4,5,6]
}
func main() {
//array
a := [3]int{1, 2, 3}
arrayCall(a)
fmt.Println(a) //[1,2,3]
//slice
s := make([]int, 3, 5)
fmt.Println(len(s), cap(s)) // 3,5
sliceCall(s)
fmt.Println(len(s), cap(s)) // 3,5
fmt.Println(s) //[3,2,1]
}
如果你的答案完全正确的话那么我要说的你应该都知道,可以跳过这一节内容了。如果不对,别怀疑,看下面分析。
首先array部分很好理解,array是值传递所以将a从main传递到arrayCall中时会在arraySlice的堆或者栈创建一个和main中a一模一样的变量并拷贝其值。因此arrayCall中的a和main中的a是两个完全独立的变量互不影响。
slice部分我从slice的结构、make函数、append函数三个地方分别分析。首先来看slice的结构。其实golang中slice也是一个结构体。他的定义可以在go源码包中的runtime/slice.go
中找到(go1.12.5中大概13行的样子,其他版本不是的话自己找找)其结构如下:
type slice struct {
array unsafe.Pointer
len int
cap int
}
我们可以看到slice是一个结构体,其中有array是一个指针指向一个数组的地址。len和cap分别描述slice的长度和容量,len()函数和cap函数对slice使用的时候其实就是读取这两个字段的值。array是一个指向数组的指针,这个数组用来存储slice中的实际数据。
make函数可以用来创建slice,其第一个参数是指定要创建的slice类型,第二个参数指定slice的长度,第三个参数指定slice的容量为可选参数若没有第三个参数则容量等于长度。make函数的创建slice的过程是先根据slice类型和容量创建一个该类型的数组其长度为容量值。再创建一个slice结构体,将其中的array字段指向刚刚创建的数组,并将len和cap字段赋值为make函数所接收到的值。make函数创建时cap可以大于len,表示为该slice预留了部分空间来扩容。
向slice插入新元素时就需要使用append函数,append函数分为两种情况。
第一种情况,slice还有有足够的空余空间,也就是cap-len>=需要插入的元素个数
,这时直接更改slice结构中的len字段并将插入的元素写入到array字段指向的数组中即可。当一切处理好后,append函数会返回修改后的slice。
第二种情况,当slice的容量不足以存放下需要插入的元素时就需要对slice进行扩容了。扩容过程如下:首先判断到 cap-len<需要插入的元素个数
,这时就需要对元素slice的底层array进行扩容了。go会帮我们创建一个新的array,保证能容纳下原来slice中的内容和需要插入的内容,然后将原array中的内容拷贝到新的array中并将需要插入的元素插入到新array的后面,然后更新len和cap字段,len字段的值为原来的len+插入元素数量
,cap的值为新创建array的长度。这些工作都完成后,append函数也同样会返回处理好的slice。需要注意的是由于重新分配了底层array这时返回的slice已经和append前的slice完全不一样了。这也就是在上面程序中sliceCall函数中第一次修改s[0]
时也修改了原slice的值,但是第二次修改s[0]
却没有修改原slice中的值。
slice扩容时新创建array的长度等于原来的len+插入元素数量
吗?从上面的程序中我们可以看出来不是的,而且也不是如上面程序所见扩容后的容量为原来容量的两倍。新分配的底层array受go内存分配的影响(这块有时间,以后再慢慢分析,欢迎大家关注我的专栏)。
我们再回过头想想上面说的slice是指针传递,对吗?其实这也是不对的。传递的时候只有底层数组是传递的指针而其中的len和cap字段都是值传递,也就是说在调用函数内部更改len和cap或者重新分配了底层array都是不会影响到原slice的,所以想在调用函数内部修改slice的len和cap或重新分配底层array(扩容)最好还是传递一个slice指针(例如 sliceCall(s *[]int)
)。
通过以上的分析再回头看看上面的程序应该都能理解了。
string与[]byte的黑魔法
在go中[]byte和string是可以进行互相装换的。但是go原生提供装换方法
//[]byte转string
bytes:=[]byte{'h','e','l','l','o'}
str:=string(bytes)
//string转[]byte
str:="hello"
bytes:=[]byte](str)
这样转换最安全。但是由于[]byte和string进行转换时会重新创建一个新的对象,存在拷贝的性能损耗。下面介绍一种无拷贝的转换方法,这种方法是从雨痕那学到的,想看原文的可以看这里,我对原方法进行了简单改善增加了可读性:
package main
import (
"fmt"
"unsafe"
)
type strType struct {
array unsafe.Pointer
len int
}
type sliceType struct {
array unsafe.Pointer
len int
cap int
}
func Slice2String(s []byte)string {
poiner:=(*sliceType)(unsafe.Pointer(&s))
var str strType
str.array=poiner.array
str.len=poiner.len
return *(*string)(unsafe.Pointer(&str))
}
func String2Slice(s string)[]byte {
poiner:=(*strType)(unsafe.Pointer(&s))
var slice sliceType
slice.array=poiner.array
slice.len=poiner.len
slice.cap=poiner.len
return *(*[]byte)(unsafe.Pointer(&slice))
}
func main() {
str:="hello"
bytes:=[]byte{'h','e','l','l','o'}
fmt.Println(String2Slice(str)) //[104 101 108 108 111]
fmt.Println(Slice2String(bytes)) //hello
}
下面看看性能提升怎么样,跑一下基准测试代码如下:
//代码依赖上面的转换函数
package main
import "testing"
func BenchmarkMySlice2String(b *testing.B) {
bytes := []byte{'h', 'e', 'l', 'l', 'o'}
for i := 0; i < b.N; i++ {
tmp := Slice2String(bytes)
_ = tmp
}
}
func BenchmarkGoSlice2String(b *testing.B) {
bytes := []byte{'h', 'e', 'l', 'l', 'o'}
for i := 0; i < b.N; i++ {
tmp := string(bytes)
_ = tmp
}
}
func BenchmarkMyString2Slice(b *testing.B) {
str := "hello"
for i := 0; i < b.N; i++ {
tmp := String2Slice(str)
_ = tmp
}
}
func BenchmarkGoString2Slice(b *testing.B) {
str := "hello"
for i := 0; i < b.N; i++ {
tmp := []byte(str)
_ = tmp
}
}
测试结果如下:
goos: windows
goarch: amd64
pkg: test/doc
BenchmarkMySlice2String-8 2000000000 0.90 ns/op
BenchmarkGoSlice2String-8 200000000 8.59 ns/op
BenchmarkMyString2Slice-8 2000000000 0.44 ns/op
BenchmarkGoString2Slice-8 100000000 10.4 ns/op
从上面结果可以看出效果还是不错的。在某些场景例如用base64来传输文件,go这边接收到一个base64的字符串而后续操作却需要用到[]byte切片。就可以使用该方法来处理不仅更快而且还能节省值拷贝带来的内存开销。
虽然这个方法看上去很棒但是却不是安全方法。需要注意string转换为slice后只能对该slice进行 读操作 不可进行修改操作,因为string只可读不可修改,尝试修改会触发 panic。还有就是unsafe.pointer不会创建GC的引用关系,如果超出了原string或slice的作用域有 被回收的风险。如果你对go的GC不了解,请谨慎使用。切记评估风险后谨慎使用。
文笔不好先写到这,下次研究一下interface。
原创作品,转载留名
参考资料
[雨痕 Go性能优化技巧 1/10] https://segmentfault.com/a/1190000005006351