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