接觸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