聲明變量的一般形式是使用 var
關(guān)鍵字:var identifier type
。
需要注意的是,Go 和許多編程語(yǔ)言不同,它在聲明變量時(shí)將變量的類(lèi)型放在變量的名稱(chēng)之后。Go 為什么要選擇這么做呢?
首先,它是為了避免像 C 語(yǔ)言中那樣含糊不清的聲明形式,例如:int* a, b;
。在這個(gè)例子中,只有 a 是指針而 b 不是。如果你想要這兩個(gè)變量都是指針,則需要將它們分開(kāi)書(shū)寫(xiě)(你可以在 Go 語(yǔ)言的聲明語(yǔ)法 頁(yè)面找到有關(guān)于這個(gè)話(huà)題的更多討論)。
而在 Go 中,則可以很輕松地將它們都聲明為指針類(lèi)型:
var a, b *int
其次,這種語(yǔ)法能夠按照從左至右的順序閱讀,使得代碼更加容易理解。
示例:
var a int
var b bool
var str string
你也可以改寫(xiě)成這種形式:
var (
a int
b bool
str string
)
這種因式分解關(guān)鍵字的寫(xiě)法一般用于聲明全局變量。
當(dāng)一個(gè)變量被聲明之后,系統(tǒng)自動(dòng)賦予它該類(lèi)型的零值:int 為 0,float 為 0.0,bool 為 false,string 為空字符串,指針為 nil。記住,所有的內(nèi)存在 Go 中都是經(jīng)過(guò)初始化的。
變量的命名規(guī)則遵循駱駝命名法,即首個(gè)單詞小寫(xiě),每個(gè)新單詞的首字母大寫(xiě),例如:numShips
和 startDate
。
但如果你的全局變量希望能夠被外部包所使用,則需要將首個(gè)單詞的首字母也大寫(xiě)(第 4.2 節(jié):可見(jiàn)性規(guī)則)。
一個(gè)變量(常量、類(lèi)型或函數(shù))在程序中都有一定的作用范圍,稱(chēng)之為作用域。如果一個(gè)變量在函數(shù)體外聲明,則被認(rèn)為是全局變量,可以在整個(gè)包甚至外部包(被導(dǎo)出后)使用,不管你聲明在哪個(gè)源文件里或在哪個(gè)源文件里調(diào)用該變量。
在函數(shù)體內(nèi)聲明的變量稱(chēng)之為局部變量,它們的作用域只在函數(shù)體內(nèi),參數(shù)和返回值變量也是局部變量。在第 5 章,我們將會(huì)學(xué)習(xí)到像 if 和 for 這些控制結(jié)構(gòu),而在這些結(jié)構(gòu)中聲明的變量的作用域只在相應(yīng)的代碼塊內(nèi)。一般情況下,局部變量的作用域可以通過(guò)代碼塊(用大括號(hào)括起來(lái)的部分)判斷。
盡管變量的標(biāo)識(shí)符必須是唯一的,但你可以在某個(gè)代碼塊的內(nèi)層代碼塊中使用相同名稱(chēng)的變量,則此時(shí)外部的同名變量將會(huì)暫時(shí)隱藏(結(jié)束內(nèi)部代碼塊的執(zhí)行后隱藏的外部同名變量又會(huì)出現(xiàn),而內(nèi)部同名變量則被釋放),你任何的操作都只會(huì)影響內(nèi)部代碼塊的局部變量。
變量可以編譯期間就被賦值,賦值給變量使用運(yùn)算符等號(hào) =
,當(dāng)然你也可以在運(yùn)行時(shí)對(duì)變量進(jìn)行賦值操作。
示例:
a = 15
b = false
一般情況下,當(dāng)變量a和變量b之間類(lèi)型相同時(shí),才能進(jìn)行如a = b
的賦值。
聲明與賦值(初始化)語(yǔ)句也可以組合起來(lái)。
示例:
var identifier [type] = value
var a int = 15
var i = 5
var b bool = false
var str string = "Go says hello to the world!"
但是 Go 編譯器的智商已經(jīng)高到可以根據(jù)變量的值來(lái)自動(dòng)推斷其類(lèi)型,這有點(diǎn)像 Ruby 和 Python 這類(lèi)動(dòng)態(tài)語(yǔ)言,只不過(guò)它們是在運(yùn)行時(shí)進(jìn)行推斷,而 Go 是在編譯時(shí)就已經(jīng)完成推斷過(guò)程。因此,你還可以使用下面的這些形式來(lái)聲明及初始化變量:
var a = 15
var b = false
var str = "Go says hello to the world!"
或:
var (
a = 15
b = false
str = "Go says hello to the world!"
numShips = 50
city string
)
不過(guò)自動(dòng)推斷類(lèi)型并不是任何時(shí)候都適用的,當(dāng)你想要給變量的類(lèi)型并不是自動(dòng)推斷出的某種類(lèi)型時(shí),你還是需要顯式指定變量的類(lèi)型,例如:
var n int64 = 2
然而,var a
這種語(yǔ)法是不正確的,因?yàn)榫幾g器沒(méi)有任何可以用于自動(dòng)推斷類(lèi)型的依據(jù)。變量的類(lèi)型也可以在運(yùn)行時(shí)實(shí)現(xiàn)自動(dòng)推斷,例如:
var (
HOME = os.Getenv("HOME")
USER = os.Getenv("USER")
GOROOT = os.Getenv("GOROOT")
)
這種寫(xiě)法主要用于聲明包級(jí)別的全局變量,當(dāng)你在函數(shù)體內(nèi)聲明局部變量時(shí),應(yīng)使用簡(jiǎn)短聲明語(yǔ)法 :=
,例如:
a := 1
下面這個(gè)例子展示了如何通過(guò)runtime
包在運(yùn)行時(shí)獲取所在的操作系統(tǒng)類(lèi)型,以及如何通過(guò) os
包中的函數(shù) os.Getenv()
來(lái)獲取環(huán)境變量中的值,并保存到 string 類(lèi)型的局部變量 path 中。
示例 4.5 goos.go
package main
import (
"fmt"
"runtime"
"os"
)
func main() {
var goos string = runtime.GOOS
fmt.Printf("The operating system is: %s\n", goos)
path := os.Getenv("PATH")
fmt.Printf("Path is %s\n", path)
}
如果你在 Windows 下運(yùn)行這段代碼,則會(huì)輸出 The operating system is: windows
以及相應(yīng)的環(huán)境變量的值;如果你在 Linux 下運(yùn)行這段代碼,則會(huì)輸出 The operating system is: linux
以及相應(yīng)的的環(huán)境變量的值。
這里用到了 Printf
的格式化輸出的功能(第 4.4.3 節(jié))。
程序中所用到的內(nèi)存在計(jì)算機(jī)中使用一堆箱子來(lái)表示(這也是人們?cè)谥v解它的時(shí)候的畫(huà)法),這些箱子被稱(chēng)為 “ 字 ”。根據(jù)不同的處理器以及操作系統(tǒng)類(lèi)型,所有的字都具有 32 位(4 字節(jié))或 64 位(8 字節(jié))的相同長(zhǎng)度;所有的字都使用相關(guān)的內(nèi)存地址來(lái)進(jìn)行表示(以十六進(jìn)制數(shù)表示)。
所有像 int、float、bool 和 string 這些基本類(lèi)型都屬于值類(lèi)型,使用這些類(lèi)型的變量直接指向存在內(nèi)存中的值:
http://wiki.jikexueyuan.com/project/the-way-to-go/images/4.4.2_fig4.1.jpg?raw=true" alt="" />
另外,像數(shù)組(第 7 章)和結(jié)構(gòu)(第 10 章)這些復(fù)合類(lèi)型也是值類(lèi)型。
當(dāng)使用等號(hào) =
將一個(gè)變量的值賦值給另一個(gè)變量時(shí),如:j = i
,實(shí)際上是在內(nèi)存中將 i 的值進(jìn)行了拷貝:
http://wiki.jikexueyuan.com/project/the-way-to-go/images/4.4.2_fig4.2.jpg?raw=true" alt="" />
你可以通過(guò) &i 來(lái)獲取變量 i 的內(nèi)存地址(第 4.9 節(jié)),例如:0xf840000040(每次的地址都可能不一樣)。值類(lèi)型的變量的值存儲(chǔ)在棧中。
內(nèi)存地址會(huì)根據(jù)機(jī)器的不同而有所不同,甚至相同的程序在不同的機(jī)器上執(zhí)行后也會(huì)有不同的內(nèi)存地址。因?yàn)槊颗_(tái)機(jī)器可能有不同的存儲(chǔ)器布局,并且位置分配也可能不同。
更復(fù)雜的數(shù)據(jù)通常會(huì)需要使用多個(gè)字,這些數(shù)據(jù)一般使用引用類(lèi)型保存。
一個(gè)引用類(lèi)型的變量 r1 存儲(chǔ)的是 r1 的值所在的內(nèi)存地址(數(shù)字),或內(nèi)存地址中第一個(gè)字所在的位置。
http://wiki.jikexueyuan.com/project/the-way-to-go/images/4.4.2_fig4.3.jpg?raw=true" alt="" />
這個(gè)內(nèi)存地址被稱(chēng)之為指針(你可以從上圖中很清晰地看到,第 4.9 節(jié)將會(huì)詳細(xì)說(shuō)明),這個(gè)指針實(shí)際上也被存在另外的某一個(gè)字中。
同一個(gè)引用類(lèi)型的指針指向的多個(gè)字可以是在連續(xù)的內(nèi)存地址中(內(nèi)存布局是連續(xù)的),這也是計(jì)算效率最高的一種存儲(chǔ)形式;也可以將這些字分散存放在內(nèi)存中,每個(gè)字都指示了下一個(gè)字所在的內(nèi)存地址。
當(dāng)使用賦值語(yǔ)句 r2 = r1
時(shí),只有引用(地址)被復(fù)制。
如果 r1 的值被改變了,那么這個(gè)值的所有引用都會(huì)指向被修改后的內(nèi)容,在這個(gè)例子中,r2 也會(huì)受到影響。
在 Go 語(yǔ)言中,指針(第 4.9 節(jié))屬于引用類(lèi)型,其它的引用類(lèi)型還包括 slices(第 7 章),maps(第 8 章)和 channel(第 13 章)。被引用的變量會(huì)存儲(chǔ)在堆中,以便進(jìn)行垃圾回收,且比棧擁有更大的內(nèi)存空間。
函數(shù) Printf
可以在 fmt 包外部使用,這是因?yàn)樗源髮?xiě)字母 P 開(kāi)頭,該函數(shù)主要用于打印輸出到控制臺(tái)。通常使用的格式化字符串作為第一個(gè)參數(shù):
func Printf(format string, list of variables to be printed)
在示例 4.5 中,格式化字符串為:"The operating system is: %s\n"
。
這個(gè)格式化字符串可以含有一個(gè)或多個(gè)的格式化標(biāo)識(shí)符,例如:%..
,其中 ..
可以被不同類(lèi)型所對(duì)應(yīng)的標(biāo)識(shí)符替換,如 %s
代表字符串標(biāo)識(shí)符、%v
代表使用類(lèi)型的默認(rèn)輸出格式的標(biāo)識(shí)符。這些標(biāo)識(shí)符所對(duì)應(yīng)的值從格式化字符串后的第一個(gè)逗號(hào)開(kāi)始按照相同順序添加,如果參數(shù)超過(guò) 1 個(gè)則同樣需要使用逗號(hào)分隔。使用這些占位符可以很好地控制格式化輸出的文本。
函數(shù) fmt.Sprintf
與 Printf
的作用是完全相同的,不過(guò)前者將格式化后的字符串以返回值的形式返回給調(diào)用者,因此你可以在程序中使用包含變量的字符串,具體例子可以參見(jiàn)示例 15.4 simple_tcp_server.go。
函數(shù) fmt.Print
和 fmt.Println
會(huì)自動(dòng)使用格式化標(biāo)識(shí)符 %v
對(duì)字符串進(jìn)行格式化,兩者都會(huì)在每個(gè)參數(shù)之間自動(dòng)增加空格,而后者還會(huì)在字符串的最后加上一個(gè)換行符。例如:
fmt.Print("Hello:", 23)
將輸出:Hello: 23
。
我們知道可以在變量的初始化時(shí)省略變量的類(lèi)型而由系統(tǒng)自動(dòng)推斷,而這個(gè)時(shí)候再在 Example 4.4.1 的最后一個(gè)聲明語(yǔ)句寫(xiě)上 var
關(guān)鍵字就顯得有些多余了,因此我們可以將它們簡(jiǎn)寫(xiě)為 a := 50
或 b := false
。
a 和 b 的類(lèi)型(int 和 bool)將由編譯器自動(dòng)推斷。
這是使用變量的首選形式,但是它只能被用在函數(shù)體內(nèi),而不可以用于全局變量的聲明與賦值。使用操作符 :=
可以高效地創(chuàng)建一個(gè)新的變量,稱(chēng)之為初始化聲明。
注意事項(xiàng)
如果在相同的代碼塊中,我們不可以再次對(duì)于相同名稱(chēng)的變量使用初始化聲明,例如:a := 20
就是不被允許的,編譯器會(huì)提示錯(cuò)誤 no new variables on left side of :=
,但是 a = 20
是可以的,因?yàn)檫@是給相同的變量賦予一個(gè)新的值。
如果你在定義變量 a 之前使用它,則會(huì)得到編譯錯(cuò)誤 undefined: a
。
如果你聲明了一個(gè)局部變量卻沒(méi)有在相同的代碼塊中使用它,同樣會(huì)得到編譯錯(cuò)誤,例如下面這個(gè)例子當(dāng)中的變量 a:
func main() {
var a string = "abc"
fmt.Println("hello, world")
}
嘗試編譯這段代碼將得到錯(cuò)誤 a declared and not used
。
此外,單純地給 a 賦值也是不夠的,這個(gè)值必須被使用,所以使用 fmt.Println("hello, world", a)
會(huì)移除錯(cuò)誤。
但是全局變量是允許聲明但不使用。
其他的簡(jiǎn)短形式為:
同一類(lèi)型的多個(gè)變量可以聲明在同一行,如:
var a, b, c int
(這是將類(lèi)型寫(xiě)在標(biāo)識(shí)符后面的一個(gè)重要原因)
多變量可以在同一行進(jìn)行賦值,如:
a, b, c = 5, 7, "abc"
上面這行假設(shè)了變量 a,b 和 c 都已經(jīng)被聲明,否則的話(huà)應(yīng)該這樣使用:
a, b, c := 5, 7, "abc"
右邊的這些值以相同的順序賦值給左邊的變量,所以 a 的值是 5
, b 的值是 7
,c 的值是 "abc"
。
這被稱(chēng)為 并行 或 同時(shí) 賦值。
如果你想要交換兩個(gè)變量的值,則可以簡(jiǎn)單地使用 a, b = b, a
。
(在 Go 語(yǔ)言中,這樣省去了使用交換函數(shù)的必要)
空白標(biāo)識(shí)符 _
也被用于拋棄值,如值 5
在:_, b = 5, 7
中被拋棄。
_
實(shí)際上是一個(gè)只寫(xiě)變量,你不能得到它的值。這樣做是因?yàn)?Go 語(yǔ)言中你必須使用所有被聲明的變量,但有時(shí)你并不需要使用從一個(gè)函數(shù)得到的所有返回值。
并行賦值也被用于當(dāng)一個(gè)函數(shù)返回多個(gè)返回值時(shí),比如這里的 val
和錯(cuò)誤 err
是通過(guò)調(diào)用 Func1
函數(shù)同時(shí)得到:val, err = Func1(var1)
。
變量除了可以在全局聲明中初始化,也可以在 init 函數(shù)中初始化。這是一類(lèi)非常特殊的函數(shù),它不能夠被人為調(diào)用,而是在每個(gè)包完成初始化后自動(dòng)執(zhí)行,并且執(zhí)行優(yōu)先級(jí)比 main 函數(shù)高。
每個(gè)源文件都只能包含一個(gè) init 函數(shù)。初始化總是以單線(xiàn)程執(zhí)行,并且按照包的依賴(lài)關(guān)系順序執(zhí)行。
一個(gè)可能的用途是在開(kāi)始執(zhí)行程序之前對(duì)數(shù)據(jù)進(jìn)行檢驗(yàn)或修復(fù),以保證程序狀態(tài)的正確性。
示例 4.6 init.go:
package trans
import "math"
var Pi float64
func init() {
Pi = 4 * math.Atan(1) // init() function computes Pi
}
在它的 init 函數(shù)中計(jì)算變量 Pi 的初始值。
示例 4.7 user_init.go 中導(dǎo)入了包 trans(需要init.go目錄為./trans/init.go)并且使用到了變量 Pi:
package main
import (
"fmt"
"./trans"
)
var twoPi = 2 * trans.Pi
func main() {
fmt.Printf("2*Pi = %g\n", twoPi) // 2*Pi = 6.283185307179586
}
init 函數(shù)也經(jīng)常被用在當(dāng)一個(gè)程序開(kāi)始之前調(diào)用后臺(tái)執(zhí)行的 goroutine,如下面這個(gè)例子當(dāng)中的 backend()
:
func init() {
// setup preparations
go backend()
}
練習(xí) 推斷以下程序的輸出,并解釋你的答案,然后編譯并執(zhí)行它們。
練習(xí) 4.1 local_scope.go:
package main
var a = "G"
func main() {
n()
m()
n()
}
func n() { print(a) }
func m() {
a := "O"
print(a)
}
練習(xí) 4.2 global_scope.go:
package main
var a = "G"
func main() {
n()
m()
n()
}
func n() {
print(a)
}
func m() {
a = "O"
print(a)
}
練習(xí) 4.3 function_calls_function.go
package main
var a string
func main() {
a = "G"
print(a)
f1()
}
func f1() {
a := "O"
print(a)
f2()
}
func f2() {
print(a)
}