鍍金池/ 教程/ GO/ 4.4 變量
4.7 strings 和 strconv 包
13.6 啟動外部命令和程序
?# 11.4 類型判斷:type-switch
12.1 讀取用戶的輸入
10.6 方法
12.2 文件讀寫
13 錯(cuò)誤處理與測試
9.3 鎖和 sync 包
12.3 文件拷貝
?# 11.7 第一個(gè)例子:使用 Sorter 接口排序
?# 11.5 測試一個(gè)值是否實(shí)現(xiàn)了某個(gè)接口
6.4 defer 和追蹤
12.10 XML 數(shù)據(jù)格式
13.10 性能調(diào)試:分析并優(yōu)化 Go 程序
?# 11.1 接口是什么
2.2 Go 環(huán)境變量
2.6 安裝目錄清單
2.5 在 Windows 上安裝 Go
11.11 Printf 和反射
1.2 語言的主要特性與發(fā)展的環(huán)境和影響因素
9.0 包(package)
7.4 切片重組(reslice)
13.2 運(yùn)行時(shí)異常和 panic
10.2 使用工廠方法創(chuàng)建結(jié)構(gòu)體實(shí)例
12.8 使用接口的實(shí)際例子:fmt.Fprintf
2.4 在 Mac OS X 上安裝 Go
3.8 Go 性能說明
7.2 切片
8.0 Map
3.1 Go 開發(fā)環(huán)境的基本要求
5.6 標(biāo)簽與 goto
6.10 使用閉包調(diào)試
9.5 自定義包和可見性
4.3 常量
?# 11.2 接口嵌套接口
6.5 內(nèi)置函數(shù)
前言
10.8 垃圾回收和 SetFinalizer
2.8 Go 解釋器
13.7 Go 中的單元測試和基準(zhǔn)測試
6.8 閉包
4.9 指針
13.1 錯(cuò)誤處理
10.1 結(jié)構(gòu)體定義
5.1 if-else 結(jié)構(gòu)
6.6 遞歸函數(shù)
9.9 通過 Git 打包和安裝
2.7 Go 運(yùn)行時(shí)(runtime)
10.7 類型的 String() 方法和格式化描述符
3.7 其它工具
9.6 為自定義包使用 godoc
11.12 接口與動態(tài)類型
13.3 從 panic 中恢復(fù)(Recover)
10.3 使用自定義包中的結(jié)構(gòu)體
11.14 結(jié)構(gòu)體、集合和高階函數(shù)
3.6 生成代碼文檔
9.2 regexp 包
4.1 文件名、關(guān)鍵字與標(biāo)識符
?# 11.6 使用方法集與接口
7.0 數(shù)組與切片
7.1 聲明和初始化
12.11 用 Gob 傳輸數(shù)據(jù)
5.5 Break 與 continue
1.1 起源與發(fā)展
?# 11 接口(Interfaces)與反射(reflection)
6.9 應(yīng)用閉包:將函數(shù)作為返回值
4.2 Go 程序的基本結(jié)構(gòu)和要素
8.6 將 map 的鍵值對調(diào)
6.11 計(jì)算函數(shù)執(zhí)行時(shí)間
5.0 控制結(jié)構(gòu)
10.5 匿名字段和內(nèi)嵌結(jié)構(gòu)體
4.6 字符串
3.0 編輯器、集成開發(fā)環(huán)境與其它工具
13.8 測試的具體例子
7.6 字符串、數(shù)組和切片的應(yīng)用
8.4 map 類型的切片
3.9 與其它語言進(jìn)行交互
7.3 For-range 結(jié)構(gòu)
9.7 使用 go install 安裝自定義包
6.0 函數(shù)
9.8 自定義包的目錄結(jié)構(gòu)、go install 和 go test
6.3 傳遞變長參數(shù)
13.9 用(測試數(shù)據(jù))表驅(qū)動測試
11.9 空接口
8.1 聲明、初始化和 make
6.2 函數(shù)參數(shù)與返回值
9.11 在 Go 程序中使用外部庫
3.3 調(diào)試器
4.5 基本類型和運(yùn)算符
?# 11.8 第二個(gè)例子:讀和寫
12.5 用 buffer 讀取文件
總結(jié):Go 中的面向?qū)ο?/span>
11.10 反射包
12.7 用 defer 關(guān)閉文件
9.4 精密計(jì)算和 big 包
4.4 變量
6.1 介紹
13.4 自定義包中的錯(cuò)誤處理和 panicking
12.4 從命令行讀取參數(shù)
9.10 Go 的外部包和項(xiàng)目
8.3 for-range 的配套用法
3.5 格式化代碼
10.4 帶標(biāo)簽的結(jié)構(gòu)體
7.5 切片的復(fù)制與追加
?# 11.3 類型斷言:如何檢測和轉(zhuǎn)換接口變量的類型
5.4 for 結(jié)構(gòu)
4.8 時(shí)間和日期
2.3 在 Linux 上安裝 Go
12 讀寫數(shù)據(jù)
6.12 通過內(nèi)存緩存來提升性能
9.1 標(biāo)準(zhǔn)庫概述
12.6 用切片讀寫文件
10 結(jié)構(gòu)(struct)與方法(method)
8.5 map 的排序
12.9 JSON 數(shù)據(jù)格式
13.5 一種用閉包處理錯(cuò)誤的模式
3.2 編輯器和集成開發(fā)環(huán)境
12.12 Go 中的密碼學(xué)
5.2 測試多返回值函數(shù)的錯(cuò)誤
6.7 將函數(shù)作為參數(shù)
8.2 測試鍵值對是否存在及刪除元素
3.4 構(gòu)建并運(yùn)行 Go 程序
2.1 平臺與架構(gòu)
5.3 switch 結(jié)構(gòu)

4.4 變量

4.4.1 簡介

聲明變量的一般形式是使用 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è)新單詞的首字母大寫,例如:numShipsstartDate

但如果你的全局變量希望能夠被外部包所使用,則需要將首個(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é))。

4.4.2 值類型和引用類型

程序中所用到的內(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)存空間。

4.4.3 打印

函數(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.SprintfPrintf 的作用是完全相同的,不過前者將格式化后的字符串以返回值的形式返回給調(diào)用者,因此你可以在程序中使用包含變量的字符串,具體例子可以參見示例 15.4 simple_tcp_server.go。

函數(shù) fmt.Printfmt.Println 會自動使用格式化標(biāo)識符 %v 對字符串進(jìn)行格式化,兩者都會在每個(gè)參數(shù)之間自動增加空格,而后者還會在字符串的最后加上一個(gè)換行符。例如:

fmt.Print("Hello:", 23)

將輸出:Hello: 23。

4.4.4 簡短形式,使用 := 賦值操作符

我們知道可以在變量的初始化時(shí)省略變量的類型而由系統(tǒng)自動推斷,而這個(gè)時(shí)候再在 Example 4.4.1 的最后一個(gè)聲明語句寫上 var 關(guān)鍵字就顯得有些多余了,因此我們可以將它們簡寫為 a := 50b := 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)。

4.4.5 init 函數(shù)

變量除了可以在全局聲明中初始化,也可以在 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)
}

鏈接