Go 語言的變量聲明格式爲:程序員
var 變量名 變量類型
變量聲明以關鍵字 var 開頭,後置變量類型,行尾無須分號。golang
以爲每行都用 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)_。數據結構
package main import ( "fmt" ) func main() { fmt.Println("str := \"c:\\Go\\bin\\go.exe\"") }
在源碼中,將字符串的值以雙引號書寫的方式是字符串的常見表達方式,被稱爲字符串字面量(string literal)。這種雙引號字面量不能跨行。若是須要在源碼中嵌入一個多行字符串時,就必須使用`字符,代碼以下:tcp
const str = ` 第一行 第二行 第三行 \r\n ` fmt.Println(str)
代碼運行結果:函數
第一行 第二行 第三行 \r\n
`叫反引號,兩個反引號間的字符串將被原樣賦值到 str 變量中。在這種方式下,反引號間換行將被做爲字符串中的換行,可是全部的轉義字符均無效,文本將會原樣輸出。性能
字符串中的每個元素叫作「字符」,在遍歷或者單個獲取字符串元素時能夠得到字符。學習
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語言變量生命期
指針(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
Go 語言還提供了另一種方法來建立指針變量,格式以下:
new(類型)
通常這樣寫:
str := new(string) *str = "ninja" fmt.Println(*str)
new() 函數能夠建立一個對應類型的指針,建立過程會分配內存。被建立的指針指向的值爲默認值。
討論變量生命期以前,先來了解下計算機組成裏兩個很是重要的概念:堆和棧。
棧(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 等語言的編譯器優化上也使用了相似的技術。
編譯器以爲變量應該分配在堆和棧上的原則是:變量是否被取地址。變量是否發生逃逸。