(四)golang變數
變數宣告
標準格式
Go 語言的變數宣告格式為:
var 變數名 變數型別
變數宣告以關鍵字 var 開頭,後置變數型別,行尾無須分號。
批量格式
覺得每行都用 var 宣告變數比較煩瑣?沒關係,還有一種為懶人提供的定義變數的方法:
var ( a int b string c []float32 d func() bool e struct { x int } )
使用關鍵字var和括號,可以將一組變數定義放在一起。
變數初始化
變數初始化的標準格式
var 變數名 型別 = 表示式 var hp int = 100
編譯器推導型別的格式
var hp = 100
短變數宣告並初始化
hp := 100
注意:在多個短變數宣告和賦值中,至少有一個新宣告的變量出現在左值中,即便其他變數名可能是重複宣告的,編譯器也不會報錯,程式碼如下:
conn, err := net.Dial("tcp", "127.0.0.1:8080") conn2, err := net.Dial("tcp", "127.0.0.1:8080")
上面的程式碼片段,編譯器不會報err重複定義。
go語言可以多個變數同時賦值,使用多重賦值時,如果不需要在左值中接收變數,可以使用匿名變數(anonymous variable)_。
golang字串
字串轉義符
package main import ( "fmt" ) func main() { fmt.Println("str := \"c:\\Go\\bin\\go.exe\"") }
定義多行字串
在原始碼中,將字串的值以雙引號書寫的方式是字串的常見表達方式,被稱為字串字面量(string literal)。這種雙引號字面量不能跨行。如果需要在原始碼中嵌入一個多行字串時,就必須使用`字元,程式碼如下:
const str = ` 第一行 第二行 第三行 \r\n ` fmt.Println(str)
程式碼執行結果:
第一行 第二行 第三行 \r\n
`叫反引號,兩個反引號間的字串將被原樣賦值到 str 變數中。在這種方式下,反引號間換行將被作為字串中的換行,但是所有的轉義字元均無效,文字將會原樣輸出。
Go語言字元型別(byte和rune)
字串中的每一個元素叫做“字元”,在遍歷或者單個獲取字串元素時可以獲得字元。
Go 語言的字元有以下兩種:
一種是 uint8 型別,或者叫 byte 型,代表了 ASCII 碼的一個字元。
另一種是 rune 型別,代表一個 UTF-8 字元。當需要處理中文、日文或者其他複合字符時,則需要用到 rune 型別。rune 型別實際是一個 int32。
使用 fmt.Printf 中的%T動詞可以輸出變數的實際型別,使用這個方法可以檢視 byte 和 rune 的本來型別,程式碼如下:
var a byte = 'a' fmt.Printf("%d %T\n", a, a) var b rune = '你' fmt.Printf("%d %T\n", b, b)
例子輸出結果:
97 uint8 20320 int32
可以發現,byte 型別的 a 變數,實際型別是 uint8,其值為 'a',對應的 ASCII 編碼為 97。
rune 型別的 b 變數的實際型別是 int32,對應的 Unicode 碼就是 20320。
Go 使用了特殊的 rune 型別來處理 Unicode,讓基於 Unicode 的文字處理更為方便,也可以使用 byte 型進行預設字串處理,效能和擴充套件性都有照顧。
go語言指標
Go語言變數生命期
指標(pointer)概念在 Go 語言中被拆分為兩個核心概念:
型別指標,允許對這個指標型別的資料進行修改。傳遞資料使用指標,而無須拷貝資料。型別指標不能進行偏移和運算。
切片,由指向起始元素的原始指標、元素數量和容量組成。
受益於這樣的約束和拆分,Go 語言的指標型別變數擁有指標的高效訪問,但又不會發生指標偏移,從而避免非法修改關鍵性資料問題。同時,垃圾回收也比較容易對不會發生偏移的指標進行檢索和回收。
切片比原始指標具備更強大的特性,更為安全。切片發生越界時,執行時會報出宕機,並打出堆疊,而原始指標只會崩潰。
每個變數在執行時都擁有一個地址,這個地址代表變數在記憶體中的位置。Go 語言中使用&作符放在變數前面對變數進行“取地址”操作。
格式如下:
ptr := &v// v的型別為T
其中 v 代表被取地址的變數,被取地址的 v 使用 ptr 變數進行接收,ptr 的型別就為T,稱做 T 的指標型別。 代表指標。
指標實際用法,通過下面的例子瞭解:
package main import ( "fmt" ) func main() { var cat int = 1 var str string = "banana" fmt.Printf("%p %p", &cat, &str) }
執行結果:
0xc042052088 0xc0420461b0
建立指標的另一種方法——new() 函式
Go 語言還提供了另外一種方法來建立指標變數,格式如下:
new(型別)
一般這樣寫:
str := new(string) *str = "ninja" fmt.Println(*str)
new() 函式可以建立一個對應型別的指標,建立過程會分配記憶體。被建立的指標指向的值為預設值。
Go語言變數生命期,Go語言變數逃逸分析
討論變數生命期之前,先來了解下計算機組成裡兩個非常重要的概念:堆和棧。
棧
棧(Stack)是一種擁有特殊規則的線性表資料結構。
1) 概念
棧只允許往線性表的一端放入資料,之後在這一端取出資料,按照後進先出(LIFO,Last InFirst Out)的順序,如下圖所示。
圖:棧的操作及擴充套件
往棧中放入元素的過程叫做入棧。入棧會增加棧的元素數量,最後放入的元素總是位於棧的頂部,最先放入的元素總是位於棧的底部。
從棧中取出元素時,只能從棧頂部取出。取出元素後,棧的數量會變少。最先放入的元素總是最後被取出,最後放入的元素總是最先被取出。不允許從棧底獲取資料,也不允許對棧成員(除棧頂外的成員)進行任何檢視和修改操作。
棧的原理類似於將書籍一本一本地堆起來。書按順序一本一本從頂部放入,要取書時只能從頂部一本一本取出。
2) 變數和棧有什麼關係
棧可用於記憶體分配,棧的分配和回收速度非常快。下面程式碼展示棧在記憶體分配上的作用,程式碼如下:
func calc(a, b int) int { var c int c = a * b var x int x = c * 10 return x }
程式碼說明如下:
第 1 行,傳入 a、b 兩個整型引數。
第 2 行,宣告 c 整型變數,執行時,c 會分配一段記憶體用以儲存 c 的數值。
第 3 行,將 a 和 b 相乘後賦予 c。
第 5 行,宣告 x 整型變數,x 也會被分配一段記憶體。
第 6 行,讓 c 乘以 10 後儲存到 x 變數中。
第 8 行,返回 x 的值。
上面的程式碼在沒有任何優化情況下,會進行 c 和 x 變數的分配過程。Go 語言預設情況下會將 c 和 x 分配在棧上,這兩個變數在 calc() 函式退出時就不再使用,函式結束時,儲存 c 和 x 的棧記憶體再出棧釋放記憶體,整個分配記憶體的過程通過棧的分配和回收都會非常迅速。
堆
堆在記憶體分配中類似於往一個房間裡擺放各種傢俱,傢俱的尺寸有大有小。分配記憶體時,需要找一塊足夠裝下傢俱的空間再擺放傢俱。經過反覆擺放和騰空傢俱後,房間裡的空間會變得亂七八糟,此時再往空間裡擺放傢俱會存在雖然有足夠的空間,但各空間分佈在不同的區域,無法有一段連續的空間來擺放傢俱的問題。此時,記憶體分配器就需要對這些空間進行調整優化,如下圖所示。
圖:堆的分配及空間
堆分配記憶體和棧分配記憶體相比,堆適合不可預知大小的記憶體分配。但是為此付出的代價是分配速度較慢,而且會形成記憶體碎片。
變數逃逸(Escape Analysis)——自動決定變數分配方式,提高執行效率
堆和棧各有優缺點,該怎麼在程式設計中處理這個問題呢?在 C/C++ 語言中,需要開發者自己學習如何進行記憶體分配,選用怎樣的記憶體分配方式來適應不同的演算法需求。比如,函式區域性變數儘量使用棧;全域性變數、結構體成員使用堆分配等。程式員不得不花費很多年的時間在不同的專案中學習、記憶這些概念並加以實踐和使用。
Go 語言將這個過程整合到編譯器中,命名為“變數逃逸分析”。這個技術由編譯器分析程式碼的特徵和程式碼生命期,決定應該如何堆還是棧進行記憶體分配,即使程式設計師使用 Go 語言完成了整個工程後也不會感受到這個過程。
1) 逃逸分析
使用下面的程式碼來展現 Go 語言如何通過命令列分析變數逃逸,程式碼如下:
package main import "fmt" // 本函式測試入口引數和返回值情況 func dummy(b int) int { // 宣告一個c賦值進入引數並返回 var c int c = b return c } // 空函式, 什麼也不做 func void() { } func main() { // 宣告a變數並列印 var a int // 呼叫void()函式 void() // 列印a變數的值和dummy()函式返回 fmt.Println(a, dummy(0)) }
程式碼說明如下:
第 6 行,dummy() 函式擁有一個引數,返回一個整型值,測試函式引數和返回值分析情況。
第 9 行,宣告 c 變數,這裡演示函式臨時變數通過函式返回值返回後的情況。
第 16 行,這是一個空函式,測試沒有任何引數函式的分析情況。
第 23 行,在 main() 中宣告 a 變數,測試 main() 中變數的分析情況。
第 26 行,呼叫 void() 函式,沒有返回值,測試 void() 呼叫後的分析情況。
第 29 行,列印 a 和 dummy(0) 的返回值,測試函式返回值沒有變數接收時的分析情況。
接著使用如下命令列執行上面的程式碼:
$ go run -gcflags "-m -l" main.go
使用 go run 執行程式時,-gcflags 引數是編譯引數。其中 -m 表示進行記憶體分配分析,-l 表示避免程式內聯,也就是避免進行程式優化。
執行結果如下:
# command-line-arguments ./main.go:29:13: a escapes to heap ./main.go:29:22: dummy(0) escapes to heap ./main.go:29:13: main ... argument does not escape 0 0
程式執行結果分析如下:
輸出第 2 行告知“main 的第 29 行的變數 a 逃逸到堆”。
第 3 行告知“dummy(0)呼叫逃逸到堆”。由於 dummy() 函式會返回一個整型值,這個值被 fmt.Println 使用後還是會在其聲明後繼續在 main() 函式中存在。
第 4 行,這句提示是預設的,可以忽略。
上面例子中變數 c 是整型,其值通過 dummy() 的返回值“逃出”了 dummy() 函式。c 變數值被複制並作為 dummy() 函式返回值返回,即使 c 變數在 dummy() 函式中分配的記憶體被釋放,也不會影響 main() 中使用 dummy() 返回的值。c 變數使用棧分配不會影響結果。
2) 取地址發生逃逸
下面的例子使用結構體做資料,瞭解在堆上分配的情況,程式碼如下:
package main import "fmt" // 宣告空結構體測試結構體逃逸情況 type Data struct { } func dummy() *Data { // 例項化c為Data型別 var c Data //返回函式區域性變數地址 return &c } func main() { fmt.Println(dummy()) }
程式碼說明如下:
第 6 行,宣告一個空的結構體做結構體逃逸分析。
第 9 行,將 dummy() 函式的返回值修改為 *Data 指標型別。
第 12 行,將 c 變數宣告為 Data 型別,此時 c 的結構體為值型別。
第 15 行,取函式區域性變數 c 的地址並返回。Go 語言的特性允許這樣做。
第 20 行,列印 dummy() 函式的返回值。
執行逃逸分析:
$ go run -gcflags "-m -l" main.go # command-line-arguments ./main.go:15:9: &c escapes to heap ./main.go:12:6: moved to heap: c ./main.go:20:19: dummy() escapes to heap ./main.go:20:13: main ... argument does not escape &{}
注意第 4 行出現了新的提示:將 c 移到堆中。這句話表示,Go 編譯器已經確認如果將 c 變數分配在棧上是無法保證程式最終結果的。如果堅持這樣做,dummy() 的返回值將是 Data 結構的一個不可預知的記憶體地址。這種情況一般是 C/C++ 語言中容易犯錯的地方:引用了一個函式區域性變數的地址。
Go 語言最終選擇將 c 的 Data 結構分配在堆上。然後由垃圾回收器去回收 c 的記憶體。
3) 原則
在使用 Go 語言進行程式設計時,Go 語言的設計者不希望開發者將精力放在記憶體應該分配在棧還是堆上的問題。編譯器會自動幫助開發者完成這個糾結的選擇。但變數逃逸分析也是需要了解的一個編譯器技術,這個技術不僅用於 Go 語言,在 Java 等語言的編譯器優化上也使用了類似的技術。
編譯器覺得變數應該分配在堆和棧上的原則是:
變數是否被取地址。
變數是否發生逃逸。