原文鏈接:https://gopl-zh.github.io/ch2/ch2-03.html
var聲明語句可以創(chuàng)建一個(gè)特定類型的變量,然后給變量附加一個(gè)名字,并且設(shè)置變量的初始值。變量聲明的一般語法如下:
var 變量名字 類型 = 表達(dá)式
其中“類型”或“= 表達(dá)式”兩個(gè)部分可以省略其中的一個(gè)。如果省略的是類型信息,那么將根據(jù)初始化表達(dá)式來推導(dǎo)變量的類型信息。如果初始化表達(dá)式被省略,那么將用零值初始化該變量。 數(shù)值類型變量對應(yīng)的零值是0,布爾類型變量對應(yīng)的零值是false,字符串類型對應(yīng)的零值是空字符串,接口或引用類型(包括slice、指針、map、chan和函數(shù))變量對應(yīng)的零值是nil。數(shù)組或結(jié)構(gòu)體等聚合類型對應(yīng)的零值是每個(gè)元素或字段都是對應(yīng)該類型的零值。
零值初始化機(jī)制可以確保每個(gè)聲明的變量總是有一個(gè)良好定義的值,因此在Go語言中不存在未初始化的變量。這個(gè)特性可以簡化很多代碼,而且可以在沒有增加額外工作的前提下確保邊界條件下的合理行為。例如:
var s string
fmt.Println(s) // ""
這段代碼將打印一個(gè)空字符串,而不是導(dǎo)致錯(cuò)誤或產(chǎn)生不可預(yù)知的行為。Go語言程序員應(yīng)該讓一些聚合類型的零值也具有意義,這樣可以保證不管任何類型的變量總是有一個(gè)合理有效的零值狀態(tài)。
也可以在一個(gè)聲明語句中同時(shí)聲明一組變量,或用一組初始化表達(dá)式聲明并初始化一組變量。如果省略每個(gè)變量的類型,將可以聲明多個(gè)類型不同的變量(類型由初始化表達(dá)式推導(dǎo)):
var i, j, k int // int, int, int
var b, f, s = true, 2.3, "four" // bool, float64, string
初始化表達(dá)式可以是字面量或任意的表達(dá)式。在包級別聲明的變量會(huì)在main入口函數(shù)執(zhí)行前完成初始化(§2.6.2),局部變量將在聲明語句被執(zhí)行到的時(shí)候完成初始化。
一組變量也可以通過調(diào)用一個(gè)函數(shù),由函數(shù)返回的多個(gè)返回值初始化:
var f, err = os.Open(name) // os.Open returns a file and an error
在函數(shù)內(nèi)部,有一種稱為簡短變量聲明語句的形式可用于聲明和初始化局部變量。它以“名字 := 表達(dá)式”形式聲明變量,變量的類型根據(jù)表達(dá)式來自動(dòng)推導(dǎo)。下面是lissajous函數(shù)中的三個(gè)簡短變量聲明語句(§1.4):
anim := gif.GIF{LoopCount: nframes}
freq := rand.Float64() * 3.0
t := 0.0
因?yàn)楹啙嵑挽`活的特點(diǎn),簡短變量聲明被廣泛用于大部分的局部變量的聲明和初始化。var形式的聲明語句往往是用于需要顯式指定變量類型的地方,或者因?yàn)樽兞可院髸?huì)被重新賦值而初始值無關(guān)緊要的地方。
i := 100 // an int
var boiling float64 = 100 // a float64
var names []string
var err error
var p Point
和var形式聲明語句一樣,簡短變量聲明語句也可以用來聲明和初始化一組變量:
i, j := 0, 1
但是這種同時(shí)聲明多個(gè)變量的方式應(yīng)該限制只在可以提高代碼可讀性的地方使用,比如for語句的循環(huán)的初始化語句部分。
請記住“:=”是一個(gè)變量聲明語句,而“=”是一個(gè)變量賦值操作。也不要混淆多個(gè)變量的聲明和元組的多重賦值(§2.4.1),后者是將右邊各個(gè)表達(dá)式的值賦值給左邊對應(yīng)位置的各個(gè)變量:
i, j = j, i // 交換 i 和 j 的值
和普通var形式的變量聲明語句一樣,簡短變量聲明語句也可以用函數(shù)的返回值來聲明和初始化變量,像下面的os.Open函數(shù)調(diào)用將返回兩個(gè)值:
f, err := os.Open(name)
if err != nil {
return err
}
// ...use f...
f.Close()
這里有一個(gè)比較微妙的地方:簡短變量聲明左邊的變量可能并不是全部都是剛剛聲明的。如果有一些已經(jīng)在相同的詞法域聲明過了(§2.7),那么簡短變量聲明語句對這些已經(jīng)聲明過的變量就只有賦值行為了。
在下面的代碼中,第一個(gè)語句聲明了in和err兩個(gè)變量。在第二個(gè)語句只聲明了out一個(gè)變量,然后對已經(jīng)聲明的err進(jìn)行了賦值操作。
in, err := os.Open(infile)
// ...
out, err := os.Create(outfile)
簡短變量聲明語句中必須至少要聲明一個(gè)新的變量,下面的代碼將不能編譯通過:
f, err := os.Open(infile)
// ...
f, err := os.Create(outfile) // compile error: no new variables
解決的方法是第二個(gè)簡短變量聲明語句改用普通的多重賦值語句。
簡短變量聲明語句只有對已經(jīng)在同級詞法域聲明過的變量才和賦值操作語句等價(jià),如果變量是在外部詞法域聲明的,那么簡短變量聲明語句將會(huì)在當(dāng)前詞法域重新聲明一個(gè)新的變量。我們在本章后面將會(huì)看到類似的例子。
一個(gè)變量對應(yīng)一個(gè)保存了變量對應(yīng)類型值的內(nèi)存空間。普通變量在聲明語句創(chuàng)建時(shí)被綁定到一個(gè)變量名,比如叫x的變量,但是還有很多變量始終以表達(dá)式方式引入,例如x[i]或x.f變量。所有這些表達(dá)式一般都是讀取一個(gè)變量的值,除非它們是出現(xiàn)在賦值語句的左邊,這種時(shí)候是給對應(yīng)變量賦予一個(gè)新的值。
一個(gè)指針的值是另一個(gè)變量的地址。一個(gè)指針對應(yīng)變量在內(nèi)存中的存儲位置。并不是每一個(gè)值都會(huì)有一個(gè)內(nèi)存地址,但是對于每一個(gè)變量必然有對應(yīng)的內(nèi)存地址。通過指針,我們可以直接讀或更新對應(yīng)變量的值,而不需要知道該變量的名字(如果變量有名字的話)。
如果用“var x int”聲明語句聲明一個(gè)x變量,那么&x表達(dá)式(取x變量的內(nèi)存地址)將產(chǎn)生一個(gè)指向該整數(shù)變量的指針,指針對應(yīng)的數(shù)據(jù)類型是*int
,指針被稱之為“指向int類型的指針”。如果指針名字為p,那么可以說“p指針指向變量x”,或者說“p指針保存了x變量的內(nèi)存地址”。同時(shí)*p
表達(dá)式對應(yīng)p指針指向的變量的值。一般*p
表達(dá)式讀取指針指向的變量的值,這里為int類型的值,同時(shí)因?yàn)?code>*p對應(yīng)一個(gè)變量,所以該表達(dá)式也可以出現(xiàn)在賦值語句的左邊,表示更新指針?biāo)赶虻淖兞康闹怠?
x := 1
p := &x // p, of type *int, points to x
fmt.Println(*p) // "1"
*p = 2 // equivalent to x = 2
fmt.Println(x) // "2"
對于聚合類型每個(gè)成員——比如結(jié)構(gòu)體的每個(gè)字段、或者是數(shù)組的每個(gè)元素——也都是對應(yīng)一個(gè)變量,因此可以被取地址。
變量有時(shí)候被稱為可尋址的值。即使變量由表達(dá)式臨時(shí)生成,那么表達(dá)式也必須能接受&
取地址操作。
任何類型的指針的零值都是nil。如果p指向某個(gè)有效變量,那么p != nil
測試為真。指針之間也是可以進(jìn)行相等測試的,只有當(dāng)它們指向同一個(gè)變量或全部是nil時(shí)才相等。
var x, y int
fmt.Println(&x == &x, &x == &y, &x == nil) // "true false false"
在Go語言中,返回函數(shù)中局部變量的地址也是安全的。例如下面的代碼,調(diào)用f函數(shù)時(shí)創(chuàng)建局部變量v,在局部變量地址被返回之后依然有效,因?yàn)橹羔榩依然引用這個(gè)變量。
var p = f()
func f() *int {
v := 1
return &v
}
每次調(diào)用f函數(shù)都將返回不同的結(jié)果:
fmt.Println(f() == f()) // "false"
因?yàn)橹羔槹艘粋€(gè)變量的地址,因此如果將指針作為參數(shù)調(diào)用函數(shù),那將可以在函數(shù)中通過該指針來更新變量的值。例如下面這個(gè)例子就是通過指針來更新變量的值,然后返回更新后的值,可用在一個(gè)表達(dá)式中(譯注:這是對C語言中++v
操作的模擬,這里只是為了說明指針的用法,incr函數(shù)模擬的做法并不推薦):
func incr(p *int) int {
*p++ // 非常重要:只是增加p指向的變量的值,并不改變p指針!??!
return *p
}
v := 1
incr(&v) // side effect: v is now 2
fmt.Println(incr(&v)) // "3" (and v is 3)
每次我們對一個(gè)變量取地址,或者復(fù)制指針,我們都是為原變量創(chuàng)建了新的別名。例如,*p
就是變量v的別名。指針特別有價(jià)值的地方在于我們可以不用名字而訪問一個(gè)變量,但是這是一把雙刃劍:要找到一個(gè)變量的所有訪問者并不容易,我們必須知道變量全部的別名(譯注:這是Go語言的垃圾回收器所做的工作)。不僅僅是指針會(huì)創(chuàng)建別名,很多其他引用類型也會(huì)創(chuàng)建別名,例如slice、map和chan,甚至結(jié)構(gòu)體、數(shù)組和接口都會(huì)創(chuàng)建所引用變量的別名。
指針是實(shí)現(xiàn)標(biāo)準(zhǔn)庫中flag包的關(guān)鍵技術(shù),它使用命令行參數(shù)來設(shè)置對應(yīng)變量的值,而這些對應(yīng)命令行標(biāo)志參數(shù)的變量可能會(huì)零散分布在整個(gè)程序中。為了說明這一點(diǎn),在早些的echo版本中,就包含了兩個(gè)可選的命令行參數(shù):-n
用于忽略行尾的換行符,-s sep
用于指定分隔字符(默認(rèn)是空格)。下面這是第四個(gè)版本,對應(yīng)包路徑為gopl.io/ch2/echo4。
gopl.io/ch2/echo4
// Echo4 prints its command-line arguments.
package main
import (
"flag"
"fmt"
"strings"
)
var n = flag.Bool("n", false, "omit trailing newline")
var sep = flag.String("s", " ", "separator")
func main() {
flag.Parse()
fmt.Print(strings.Join(flag.Args(), *sep))
if !*n {
fmt.Println()
}
}
調(diào)用flag.Bool函數(shù)會(huì)創(chuàng)建一個(gè)新的對應(yīng)布爾型標(biāo)志參數(shù)的變量。它有三個(gè)屬性:第一個(gè)是命令行標(biāo)志參數(shù)的名字“n”,然后是該標(biāo)志參數(shù)的默認(rèn)值(這里是false),最后是該標(biāo)志參數(shù)對應(yīng)的描述信息。如果用戶在命令行輸入了一個(gè)無效的標(biāo)志參數(shù),或者輸入-h
或-help
參數(shù),那么將打印所有標(biāo)志參數(shù)的名字、默認(rèn)值和描述信息。類似的,調(diào)用flag.String函數(shù)將創(chuàng)建一個(gè)對應(yīng)字符串類型的標(biāo)志參數(shù)變量,同樣包含命令行標(biāo)志參數(shù)對應(yīng)的參數(shù)名、默認(rèn)值、和描述信息。程序中的sep
和n
變量分別是指向?qū)?yīng)命令行標(biāo)志參數(shù)變量的指針,因此必須用*sep
和*n
形式的指針語法間接引用它們。
當(dāng)程序運(yùn)行時(shí),必須在使用標(biāo)志參數(shù)對應(yīng)的變量之前先調(diào)用flag.Parse函數(shù),用于更新每個(gè)標(biāo)志參數(shù)對應(yīng)變量的值(之前是默認(rèn)值)。對于非標(biāo)志參數(shù)的普通命令行參數(shù)可以通過調(diào)用flag.Args()函數(shù)來訪問,返回值對應(yīng)一個(gè)字符串類型的slice。如果在flag.Parse函數(shù)解析命令行參數(shù)時(shí)遇到錯(cuò)誤,默認(rèn)將打印相關(guān)的提示信息,然后調(diào)用os.Exit(2)終止程序。
讓我們運(yùn)行一些echo測試用例:
$ go build gopl.io/ch2/echo4
$ ./echo4 a bc def
a bc def
$ ./echo4 -s / a bc def
a/bc/def
$ ./echo4 -n a bc def
a bc def$
$ ./echo4 -help
Usage of ./echo4:
-n omit trailing newline
-s string
separator (default " ")
另一個(gè)創(chuàng)建變量的方法是調(diào)用內(nèi)建的new函數(shù)。表達(dá)式new(T)將創(chuàng)建一個(gè)T類型的匿名變量,初始化為T類型的零值,然后返回變量地址,返回的指針類型為?*T
?。
p := new(int) // p, *int 類型, 指向匿名的 int 變量
fmt.Println(*p) // "0"
*p = 2 // 設(shè)置 int 匿名變量的值為 2
fmt.Println(*p) // "2"
用new創(chuàng)建變量和普通變量聲明語句方式創(chuàng)建變量沒有什么區(qū)別,除了不需要聲明一個(gè)臨時(shí)變量的名字外,我們還可以在表達(dá)式中使用new(T)。換言之,new函數(shù)類似是一種語法糖,而不是一個(gè)新的基礎(chǔ)概念。
下面的兩個(gè)newInt函數(shù)有著相同的行為:
func newInt() *int {
return new(int)
}
func newInt() *int {
var dummy int
return &dummy
}
每次調(diào)用new函數(shù)都是返回一個(gè)新的變量的地址,因此下面兩個(gè)地址是不同的:
p := new(int)
q := new(int)
fmt.Println(p == q) // "false"
當(dāng)然也可能有特殊情況:如果兩個(gè)類型都是空的,也就是說類型的大小是0,例如struct{}
和[0]int
,有可能有相同的地址(依賴具體的語言實(shí)現(xiàn))(譯注:請謹(jǐn)慎使用大小為0的類型,因?yàn)槿绻愋偷拇笮?的話,可能導(dǎo)致Go語言的自動(dòng)垃圾回收器有不同的行為,具體請查看runtime.SetFinalizer
函數(shù)相關(guān)文檔)。
new函數(shù)使用通常相對比較少,因?yàn)閷τ诮Y(jié)構(gòu)體來說,直接用字面量語法創(chuàng)建新變量的方法會(huì)更靈活(§4.4.1)。
由于new只是一個(gè)預(yù)定義的函數(shù),它并不是一個(gè)關(guān)鍵字,因此我們可以將new名字重新定義為別的類型。例如下面的例子:
func delta(old, new int) int { return new - old }
由于new被定義為int類型的變量名,因此在delta函數(shù)內(nèi)部是無法使用內(nèi)置的new函數(shù)的。
變量的生命周期指的是在程序運(yùn)行期間變量有效存在的時(shí)間段。對于在包一級聲明的變量來說,它們的生命周期和整個(gè)程序的運(yùn)行周期是一致的。而相比之下,局部變量的生命周期則是動(dòng)態(tài)的:每次從創(chuàng)建一個(gè)新變量的聲明語句開始,直到該變量不再被引用為止,然后變量的存儲空間可能被回收。函數(shù)的參數(shù)變量和返回值變量都是局部變量。它們在函數(shù)每次被調(diào)用的時(shí)候創(chuàng)建。
例如,下面是從1.4節(jié)的Lissajous程序摘錄的代碼片段:
for t := 0.0; t < cycles*2*math.Pi; t += res {
x := math.Sin(t)
y := math.Sin(t*freq + phase)
img.SetColorIndex(size+int(x*size+0.5), size+int(y*size+0.5),
blackIndex)
}
譯注:函數(shù)的右小括弧也可以另起一行縮進(jìn),同時(shí)為了防止編譯器在行尾自動(dòng)插入分號而導(dǎo)致的編譯錯(cuò)誤,可以在末尾的參數(shù)變量后面顯式插入逗號。像下面這樣:
for t := 0.0; t < cycles*2*math.Pi; t += res {
x := math.Sin(t)
y := math.Sin(t*freq + phase)
img.SetColorIndex(
size+int(x*size+0.5), size+int(y*size+0.5),
blackIndex, // 最后插入的逗號不會(huì)導(dǎo)致編譯錯(cuò)誤,這是Go編譯器的一個(gè)特性
) // 小括弧另起一行縮進(jìn),和大括弧的風(fēng)格保存一致
}
在每次循環(huán)的開始會(huì)創(chuàng)建臨時(shí)變量t,然后在每次循環(huán)迭代中創(chuàng)建臨時(shí)變量x和y。
那么Go語言的自動(dòng)垃圾收集器是如何知道一個(gè)變量是何時(shí)可以被回收的呢?這里我們可以避開完整的技術(shù)細(xì)節(jié),基本的實(shí)現(xiàn)思路是,從每個(gè)包級的變量和每個(gè)當(dāng)前運(yùn)行函數(shù)的每一個(gè)局部變量開始,通過指針或引用的訪問路徑遍歷,是否可以找到該變量。如果不存在這樣的訪問路徑,那么說明該變量是不可達(dá)的,也就是說它是否存在并不會(huì)影響程序后續(xù)的計(jì)算結(jié)果。
因?yàn)橐粋€(gè)變量的有效周期只取決于是否可達(dá),因此一個(gè)循環(huán)迭代內(nèi)部的局部變量的生命周期可能超出其局部作用域。同時(shí),局部變量可能在函數(shù)返回之后依然存在。
編譯器會(huì)自動(dòng)選擇在棧上還是在堆上分配局部變量的存儲空間,但可能令人驚訝的是,這個(gè)選擇并不是由用var還是new聲明變量的方式?jīng)Q定的。
var global *int
func f() {
var x int
x = 1
global = &x
}
func g() {
y := new(int)
*y = 1
}
f函數(shù)里的x變量必須在堆上分配,因?yàn)樗诤瘮?shù)退出后依然可以通過包一級的global變量找到,雖然它是在函數(shù)內(nèi)部定義的;用Go語言的術(shù)語說,這個(gè)x局部變量從函數(shù)f中逃逸了。相反,當(dāng)g函數(shù)返回時(shí),變量*y
將是不可達(dá)的,也就是說可以馬上被回收的。因此,*y
并沒有從函數(shù)g中逃逸,編譯器可以選擇在棧上分配*y
的存儲空間(譯注:也可以選擇在堆上分配,然后由Go語言的GC回收這個(gè)變量的內(nèi)存空間),雖然這里用的是new方式。其實(shí)在任何時(shí)候,你并不需為了編寫正確的代碼而要考慮變量的逃逸行為,要記住的是,逃逸的變量需要額外分配內(nèi)存,同時(shí)對性能的優(yōu)化可能會(huì)產(chǎn)生細(xì)微的影響。
Go語言的自動(dòng)垃圾收集器對編寫正確的代碼是一個(gè)巨大的幫助,但也并不是說你完全不用考慮內(nèi)存了。你雖然不需要顯式地分配和釋放內(nèi)存,但是要編寫高效的程序你依然需要了解變量的生命周期。例如,如果將指向短生命周期對象的指針保存到具有長生命周期的對象中,特別是保存到全局變量時(shí),會(huì)阻止對短生命周期對象的垃圾回收(從而可能影響程序的性能)。
更多建議: