## 4.4.1 簡介
聲明變量的一般形式是使用?`var`?關鍵字:`var identifier type`。
需要注意的是,Go 和許多編程語言不同,它在聲明變量時將變量的類型放在變量的名稱之后。Go 為什么要選擇這么做呢?
首先,它是為了避免像 C 語言中那樣含糊不清的聲明形式,例如:`int* a, b;`。在這個例子中,只有 a 是指針而 b 不是。如果你想要這兩個變量都是指針,則需要將它們分開書寫(你可以在?[Go 語言的聲明語法](http://blog.golang.org/2010/07/gos-declaration-syntax.html)?頁面找到有關于這個話題的更多討論)。
而在 Go 中,則可以和輕松地將它們都聲明為指針類型:
~~~
var a, b *int
~~~
其次,這種語法能夠按照從左至右的順序閱讀,使得代碼更加容易理解。
示例:
~~~
var a int
var b bool
var str string
~~~
你也可以改寫成這種形式:
~~~
var (
a int
b bool
str string
)
~~~
這種因式分解關鍵字的寫法一般用于聲明全局變量。
當一個變量被聲明之后,系統自動賦予它該類型的零值:int 為 0,float 為 0.0,bool 為 false,string 為空字符串,指針為 nil。記住,所有的內存在 Go 中都是經過初始化的。
變量的命名規則遵循駱駝命名法,即首個單詞小寫,每個新單詞的首字母大寫,例如:`numShips`?和?`startDate`。
但如果你的全局變量希望能夠被外部包所使用,則需要將首個單詞的首字母也大寫(第 4.2 節:可見性規則)。
一個變量(常量、類型或函數)在程序中都有一定的作用范圍,稱之為作用域。如果一個變量在函數體外聲明,則被認為是全局變量,可以在整個包甚至外部包(被導出后)使用,不管你聲明在哪個源文件里或在哪個源文件里調用該變量。
在函數體內聲明的變量稱之為局部變量,它們的作用域只在函數體內,參數和返回值變量也是局部變量。在第 5 章,我們將會學習到像 if 和 for 這些控制結構,而在這些結構中聲明的變量的作用域只在相應的代碼塊內。一般情況下,局部變量的作用域可以通過代碼塊(用大括號括起來的部分)判斷。
盡管變量的標識符必須是唯一的,但你可以在某個代碼塊的內層代碼塊中使用相同名稱的變量,則此時外部的同名變量將會暫時隱藏(結束內部代碼塊的執行后隱藏的外部同名變量又會出現,而內部同名變量則被釋放),你任何的操作都只會影響內部代碼塊的局部變量。
變量可以編譯期間就被賦值,賦值給變量使用運算符等號?`=`,當然你也可以在運行時對變量進行賦值操作。
示例:
~~~
a = 15
b = false
~~~
一般情況下,只有類型相同的變量之間才可以相互賦值,例如:
~~~
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 編譯器的智商已經高到可以根據變量的值來自動推斷其類型,這有點像 Ruby 和 Python 這類動態語言,只不過它們是在運行時進行推斷,而 Go 是在編譯時就已經完成推斷過程。因此,你還可以使用下面的這些形式來聲明及初始化變量:
~~~
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
)
~~~
不過自動推斷類型并不是任何時候都適用的,當你想要給變量的類型并不是自動推斷出的某種類型時,你還是需要顯式指定變量的類型,例如:
~~~
var n int64 = 2
~~~
然而,`var a`?這種語法是不正確的,因為編譯器沒有任何可以用于自動推斷類型的依據。變量的類型也可以在運行時實現自動推斷,例如:
~~~
var (
HOME = os.Getenv("HOME")
USER = os.Getenv("USER")
GOROOT = os.Getenv("GOROOT")
)
~~~
這種寫法主要用于聲明包級別的全局變量,當你在函數體內聲明局部變量時,應使用簡短聲明語法?`:=`,例如:
~~~
a := 1
~~~
下面這個例子展示了如何在運行時獲取所在的操作系統類型,它通過?`os`?包中的函數?`os.Getenv()`?來獲取環境變量中的值,并保存到 string 類型的局部變量 path 中。
示例 4.5?[goos.go](https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/examples/chapter_4/goos.go)
~~~
package main
import (
"fmt"
"os"
)
func main() {
var goos string = os.Getenv("GOOS")
fmt.Printf("The operating system is: %s\n", goos)
path := os.Getenv("PATH")
fmt.Printf("Path is %s\n", path)
}
~~~
如果你在 Windows 下運行這段代碼,則會輸出?`The operating system is: windows`?以及相應的環境變量的值;如果你在 Linux 下運行這段代碼,則會輸出?`The operating system is: linux`?以及相應的的環境變量的值。
這里用到了?`Printf`?的格式化輸出的功能(第 4.4.3 節)。
## [](https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/04.4.md#442-值類型和引用類型)4.4.2 值類型和引用類型
程序中所用到的內存在計算機中使用一堆箱子來表示(這也是人們在講解它的時候的畫法),這些箱子被稱為 “ 字 ”。根據不同的處理器以及操作系統類型,所有的字都具有 32 位(4 字節)或 64 位(8 字節)的相同長度;所有的字都使用相關的內存地址來進行表示(以十六進制數表示)。
所有像 int、float、bool 和 string 這些基本類型都屬于值類型,使用這些類型的變量直接指向存在內存中的值:
[](https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/images/4.4.2_fig4.1.jpg?raw=true)
另外,像數組(第 7 章)和結構(第 10 章)這些復合類型也是值類型。
當使用等號?`=`?將一個變量的值賦值給另一個變量時,如:`j = i`,實際上是在內存中將 i 的值進行了拷貝:
[](https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/images/4.4.2_fig4.2.jpg?raw=true)
你可以通過 &i 來獲取變量 i 的內存地址(第 4.9 節),例如:0xf840000040(每次的地址都可能不一樣)。值類型的變量的值存儲在棧中。
內存地址會根據機器的不同而有所不同,甚至相同的程序在不同的機器上執行后也會有不同的內存地址。因為每臺機器可能有不同的存儲器布局,并且位置分配也可能不同。
更復雜的數據通常會需要使用多個字,這些數據一般使用引用類型保存。
一個引用類型的變量 r1 存儲的是 r1 的值所在的內存地址(數字),或內存地址中第一個字所在的位置。
[](https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/images/4.4.2_fig4.3.jpg?raw=true)
這個內存地址為稱之為指針(你可以從上圖中很清晰地看到,第 4.9 節將會詳細說明),這個指針實際上也被存在另外的某一個字中。
同一個引用類型的指針指向的多個字可以是在連續的內存地址中(內存布局是連續的),這也是計算效率最高的一種存儲形式;也可以將這些字分散存放在內存中,每個字都指示了下一個字所在的內存地址。
當使用賦值語句?`r2 = r1`?時,只有引用(地址)被復制。
如果 r1 的值被改變了,那么這個值的所有引用都會指向被修改后的內容,在這個例子中,r2 也會受到影響。
在 Go 語言中,指針(第 4.9 節)屬于引用類型,其它的引用類型還包括 slices(第 7 章),maps(第 8 章)和 channel(第 13 章)。被引用的變量會存儲在堆中,以便進行垃圾回收,且比棧擁有更大的內存空間。
## [](https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/04.4.md#443-打印)4.4.3 打印
函數?`Printf`?可以在 fmt 包外部使用,這是因為它以大寫字母 P 開頭,該函數主要用于打印輸出到控制臺。通常使用的格式化字符串作為第一個參數:
~~~
func Printf(format string, list of variables to be printed)
~~~
在示例 4.5 中,格式化字符串為:`"The operating system is: %s\n"`。
這個格式化字符串可以含有一個或多個的格式化標識符,例如:`%..`,其中?`..`?可以被不同類型所對應的標識符替換,如`%s`?代表字符串標識符、`%v`?代表使用類型的默認輸出格式的標識符。這些標識符所對應的值從格式化字符串后的第一個逗號開始按照相同順序添加,如果參數超過 1 個則同樣需要使用逗號分隔。使用這些占位符可以很好地控制格式化輸出的文本。
函數?`fmt.Sprintf`?與?`Printf`?的作用是完全相同的,不過前者將格式化后的字符串以返回值的形式返回給調用者,因此你可以在程序中使用包含變量的字符串,具體例子可以參見示例 15.4?[simple_tcp_server.go](https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/examples/chapter_15/simple_tcp_server.go)。
函數?`fmt.Print`?和?`fmt.Println`?會自動使用格式化標識符?`%v`?對字符串進行格式化,兩者都會在每個參數之間自動增加空格,而后者還會在字符串的最后加上一個換行符。例如:
~~~
fmt.Print("Hello:", 23)
~~~
將輸出:`Hello: 23`。
## [](https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/04.4.md#444-簡短形式使用--賦值操作符)4.4.4 簡短形式,使用 := 賦值操作符
我們知道可以在變量的初始化時省略變量的類型而由系統自動推斷,而這個時候再在 Example 4.4.1 的最后一個聲明語句寫上?`var`?關鍵字就顯得有些多余了,因此我們可以將它們簡寫為?`a := 50`?或?`b := false`。
a 和 b 的類型(int 和 bool)將由編譯器自動推斷。
這是使用變量的首選形式,但是它只能被用在函數體內,而不可以用于全局變量的聲明與賦值。使用操作符?`:=`?可以高效地創建一個新的變量,稱之為初始化聲明。
**注意事項**
如果在相同的代碼塊中,我們不可以再次對于相同名稱的變量使用初始化聲明,例如:`a := 20`?就是不被允許的,編譯器會提示錯誤?`no new variables on left side of :=`,但是?`a = 20`?是可以的,因為這是給相同的變量賦予一個新的值。
如果你在定義變量 a 之前使用它,則會得到編譯錯誤?`undefined: a`。
如果你聲明了一個局部變量卻沒有在相同的代碼塊中使用它,同樣會得到編譯錯誤,例如下面這個例子當中的變量 a:
~~~
func main() {
var a string = "abc"
fmt.Println("hello, world")
}
~~~
嘗試編譯這段代碼將得到錯誤?`a declared and not used`。
此外,單純地給 a 賦值也是不夠的,這個值必須被使用,所以使用?`fmt.Println("hello, world", a)`?會移除錯誤。
但是全局變量是允許聲明但不使用。
其他的簡短形式為:
同一類型的多個變量可以聲明在同一行,如:
~~~
var a, b, c int
~~~
(這是將類型寫在標識符后面的一個重要原因)
多變量可以在同一行進行賦值,如:
~~~
a, b, c = 5, 7, "abc"
~~~
上面這行假設了變量 a,b 和 c 都已經被聲明,否則的話應該這樣使用:
~~~
a, b, c := 5, 7, "abc"
~~~
右邊的這些值以相同的順序賦值給左邊的變量,所以 a 的值是?`5`, b 的值是?`7`,c 的值是?`"abc"`。
這被稱為?**并行**?或?**同時**?賦值。
如果你想要交換兩個變量的值,則可以簡單地使用?`a, b = b, a`。
(在 Go 語言中,這樣省去了使用交換函數的必要)
空白標識符?`_`?也被用于拋棄值,如值?`5`?在:`_, b = 5, 7`?中被拋棄。
`_`?實際上是一個只寫變量,你不能得到它的值。這樣做是因為 Go 語言中你必須使用所有被聲明的變量,但有時你并不需要使用從一個函數得到的所有返回值。
并行賦值也被用于當一個函數返回多個返回值時,比如這里的?`val`?和錯誤?`err`?是通過調用?`Func1`?函數同時得到:`val, err = Func1(var1)`。
## [](https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/04.4.md#445-init-函數)4.4.5 init 函數
變量除了可以在全局聲明中初始化,也可以在 init 函數中初始化。這是一類非常特殊的函數,它不能夠被人為調用,而是在每個包完成初始化后自動執行,并且執行優先級比 main 函數高。
每一個源文件都可以包含且只包含一個 init 函數。初始化總是以單線程執行,并且按照包的依賴關系順序執行。
一個可能的用途是在開始執行程序之前對數據進行檢驗或修復,以保證程序狀態的正確性。
示例 4.6?[init.go](https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/examples/chapter_4/init.go):
~~~
package trans
import "math"
var Pi float64
func init() {
Pi = 4 * math.Atan(1) // init() function computes Pi
}
~~~
在它的 init 函數中計算變量 Pi 的初始值。
示例 4.7?[user_init.go](https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/examples/chapter_4/user_init.go)?中導入了包 trans(在相同的路徑中)并且使用到了變量 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 函數也經常被用在當一個程序開始之前調用后臺執行的 goroutine,如下面這個例子當中的?`backend()`:
~~~
func init() {
// setup preparations
go backend()
}
~~~
**練習**?推斷以下程序的輸出,并解釋你的答案,然后編譯并執行它們。
練習 4.1?[local_scope.go](https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/examples/chapter_4/local_scope.go):
~~~
package main
var a = "G"
func main() {
n()
m()
n()
}
func n() { print(a) }
func m() {
a := "O"
print(a)
}
~~~
練習 4.2?[global_scope.go](https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/examples/chapter_4/global_scope.go):
~~~
package main
var a = "G"
func main() {
n()
m()
n()
}
func n() {
print(a)
}
func m() {
a = "O"
print(a)
}
~~~
練習 4.3?[function_calls_function.go](https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/examples/chapter_4/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)
}
~~~
- 前言
- 第一部分:學習 Go 語言
- 第1章:Go 語言的起源,發展與普及
- 1.1 起源與發展
- 1.2 語言的主要特性與發展的環境和影響因素
- 第2章:安裝與運行環境
- 2.1 平臺與架構
- 2.2 Go 環境變量
- 2.3 在 Linux 上安裝 Go
- 2.4 在 Mac OS X 上安裝 Go
- 2.5 在 Windows 上安裝 Go
- 2.6 安裝目錄清單
- 2.7 Go 運行時(runtime)
- 2.8 Go 解釋器
- 第3章:編輯器、集成開發環境與其它工具
- 3.1 Go 開發環境的基本要求
- 3.2 編輯器和集成開發環境
- 3.3 調試器
- 3.4 構建并運行 Go 程序
- 3.5 格式化代碼
- 3.6 生成代碼文檔
- 3.7 其它工具
- 3.8 Go 性能說明
- 3.9 與其它語言進行交互
- 第二部分:語言的核心結構與技術
- 第4章:基本結構和基本數據類型
- 4.1 文件名、關鍵字與標識符
- 4.2 Go 程序的基本結構和要素
- 4.3 常量
- 4.4 變量
- 4.5 基本類型和運算符
- 4.6 字符串
- 4.7 strings 和 strconv 包
- 4.8 時間和日期
- 4.9 指針
- 第5章:控制結構
- 5.1 if-else 結構
- 5.2 測試多返回值函數的錯誤
- 5.3 switch 結構
- 5.4 for 結構
- 5.5 Break 與 continue
- 5.6 標簽與 goto
- 第6章:函數(function)
- 6.1 介紹
- 6.2 函數參數與返回值
- 6.3 傳遞變長參數
- 6.4 defer 和追蹤
- 6.5 內置函數
- 6.6 遞歸函數
- 6.7 將函數作為參數
- 6.8 閉包
- 6.9 應用閉包:將函數作為返回值
- 6.10 使用閉包調試
- 6.11 計算函數執行時間
- 6.12 通過內存緩存來提升性能
- 第7章:數組與切片
- 7.1 聲明和初始化
- 7.2 切片
- 7.3 For-range 結構
- 7.4 切片重組(reslice)
- 7.5 切片的復制與追加
- 7.6 字符串、數組和切片的應用
- 第8章:Map
- 8.1 聲明、初始化和 make
- 8.2 測試鍵值對是否存在及刪除元素
- 8.3 for-range 的配套用法
- 8.4 map 類型的切片
- 8.5 map 的排序
- 8.6 將 map 的鍵值對調
- 第9章:包(package)
- 9.1 標準庫概述
- 9.2 regexp 包
- 9.3 鎖和 sync 包
- 9.4 精密計算和 big 包
- 9.5 自定義包和可見性
- 9.6 為自定義包使用 godoc
- 9.7 使用 go install 安裝自定義包
- 9.8 自定義包的目錄結構、go install 和 go test
- 9.9 通過 Git 打包和安裝
- 9.10 Go 的外部包和項目
- 9.11 在 Go 程序中使用外部庫
- 第10章:結構(struct)與方法(method)
- 10.1 結構體定義
- 10.2 使用工廠方法創建結構體實例
- 10.3 使用自定義包中的結構體
- 10.4 帶標簽的結構體
- 10.5 匿名字段和內嵌結構體
- 10.6 方法
- 10.8 垃圾回收和 SetFinalizer
- 第11章:接口(interface)與反射(reflection)
- 11.1 接口是什么
- 11.2 接口嵌套接口
- 11.3 類型斷言:如何檢測和轉換接口變量的類型
- 11.4 類型判斷:type-switch
- 11.5 測試一個值是否實現了某個接口
- 11.6 使用方法集與接口
- 11.7 第一個例子:使用 Sorter 接口排序
- 11.8 第二個例子:讀和寫
- 11.9 空接口
- 11.10 反射包
- 第三部分:Go 高級編程
- 第12章 讀寫數據
- 12.1 讀取用戶的輸入
- 12.2 文件讀寫
- 12.3 文件拷貝
- 12.4 從命令行讀取參數
- 12.5 用buffer讀取文件
- 12.6 用切片讀寫文件
- 12.7 用 defer 關閉文件
- 12.8 使用接口的實際例子:fmt.Fprintf
- 12.9 Json 數據格式
- 12.10 XML 數據格式
- 12.11 用 Gob 傳輸數據
- 12.12 Go 中的密碼學
- 第13章 錯誤處理與測試
- 13.1 錯誤處理
- 13.2 運行時異常和 panic
- 13.3 從 panic 中恢復(Recover)
- 13.4 自定義包中的錯誤處理和 panicking
- 13.5 一種用閉包處理錯誤的模式
- 13.6 啟動外部命令和程序
- 13.7 Go 中的單元測試和基準測試
- 13.8 測試的具體例子
- 13.9 用(測試數據)表驅動測試
- 13.10 性能調試:分析并優化 Go 程序
- 第14章:協程(goroutine)與通道(channel)
- 14.1 并發、并行和協程
- 14.2 使用通道進行協程間通信
- 14.3 協程同步:關閉通道-對阻塞的通道進行測試
- 14.4 使用 select 切換協程
- 14.5 通道,超時和計時器(Ticker)
- 14.6 協程和恢復(recover)
- 第15章:網絡、模版與網頁應用
- 15.1 tcp服務器
- 15.2 一個簡單的web服務器
- 15.3 訪問并讀取頁面數據
- 15.4 寫一個簡單的網頁應用
- 第四部分:實際應用
- 第16章:常見的陷阱與錯誤
- 16.1 誤用短聲明導致變量覆蓋
- 16.2 誤用字符串
- 16.3 發生錯誤時使用defer關閉一個文件
- 16.5 不需要將一個指向切片的指針傳遞給函數
- 16.6 使用指針指向接口類型
- 16.7 使用值類型時誤用指針
- 16.8 誤用協程和通道
- 16.9 閉包和協程的使用
- 16.10 糟糕的錯誤處理
- 第17章:模式
- 17.1 關于逗號ok模式
- 第18章:出于性能考慮的實用代碼片段
- 18.1 字符串
- 18.2 數組和切片
- 18.3 映射
- 18.4 結構體
- 18.5 接口
- 18.6 函數
- 18.7 文件
- 18.8 協程(goroutine)與通道(channel)
- 18.9 網絡和網頁應用
- 18.10 其他
- 18.11 出于性能考慮的最佳實踐和建議
- 附錄