和其他語言沒有區別,Go 中的數據也是兩種表示方式,常量和變量。
本文先說說變量吧。
為了增加文章的趣味性(乏味性,多掉些頭髮),文末蒐集了一些面試題。部分是自己瞎編的,順便為自己明年可能到來的面試做些準備。
什麼是變量
變量是可以理解為使用一個名稱綁定一個用來存放數據的內存塊。
變量,首先是量,即為數據,而後是變,即表示內存塊中的數據是可變的。與變量相對的是常量,常即恆常,表示數據不可變。常量的值是在編譯期就確定了。
變量的定義
Go 中變量的定義有多種方式,先看一個變量完整的定義組成。如下:
變量名稱 變量類型 變量值 var varName typeName [= Value]
var 是 Go 提供的用於定義變量的關鍵詞,變量的定義語句可出現在函數和包級別中。
語句中核心是三個部分,分別是變量的名稱、類型和值。與 C/C++ 不同,Go 的變量類型是在變量名稱之後。
定義一個變量:
var i int
var 除了定義單個變量,還可以一次定義多個變量。
// 相同類型簡寫 var i, j int // 定義不同類型變量 var ( i int s string )
初始化
變量定義時可以指定初始值。
var i int = 1 var f float64 = 1.1 var s string = "string"
變量值的可選範圍由變量類型決定。Go 是靜態語言,變量類型是不可修改的。
var i int var s string
如果變量定義時,沒有指定初始值,將自動初始化為相應的零值(不同類型,零值不同),避免類似 C/C++ 中不可預測的行為。
在 windows 上的 "燙燙燙" 的梗,就是和變量未初始化有關。
如果定義時,指定初始值,則可以省略類型,Go 編譯器會自動推導變量類型。
var i = 1 // 同時定義初始化兩個不同類型變量 var f, s = 1.1, "string"
簡短定義
在函數中,變量的定義有一種簡短寫法,:=。在初始化值類型明確的情況下,代替 var,實現類似動態語言的效果,懶人神器。
i := 1
變量類型由編譯器根據初始化值自動推導。
要注意的是,函數外的每個語句都必須以關鍵字開始(var, func 等),簡短模式不能在函數外使用。
簡短模式下,如果語句左邊有多個變量,其中包含已定義變量,且必須是位於當前的作用域,則已定義變量會轉化為賦值行為。
var x = 1 fmt.Println(&x, x) x, y := 10, 20 fmt.Println(&x, x)
運行代碼將會發現,x 的值修改了,但地址並未改變。
多變量賦值
定義變量時,已經演示瞭如何同時為多個變量賦初始值。動態語言通常支持這種寫法,比如 Pyhon。
x, y := 10, 20 x, y = x+10, y+20
這種語法在簡化寫法的同時,還有一個比較有用的點,變量交換。
通常,交換變量的寫法:
t := x x = y y = t
引入一個臨時變量實現交換。除此之外,還有兩種比較常見的交換算法,不引入臨時變量。
x = x + y y = x - y x = x - y
或者
a = a^b b = b^a a = a^b
有了多變量同時賦值的特性之後,如下的寫法即可完成交換。
x, y = y, x
匿名變量
Go 語言中會將定義但未使用的變量當成錯誤。但是有一種情況,如果 Go 的函數允許返回多個值,就要定義多個變量接收。
假設,有函數定義如下:
func row() (string, int) { return "poloxue", 18 }
現在 main 函數將打印第一個返回值,第二個返回值不會使用。
func main() { name, age := row() fmt.Println(name) }
編譯無法通過,提示存在未使用的變量。
這時,可以使用 Go 中提供的匿名變量 _ 接收無用的返回值。
name, _ := row()
匿名變量可以多次使用,不佔內存空間。
變量作用域
變量作用域和生命週期不同,生命週期表示變量執行期間的存活時間,而作用域表示變量能有效使用的範圍。
除了變量有作用範圍,還有諸如常量、函數、類型等都是有作用域的。
Go 的作用域可分為全局和局部,變量也就有全局變量和局部變量。但細究起來,全局和局部變量的說法也不對,Go 中內置的常量、函數、類型才能算是全局。變量的全局只能算包級別,包級別變量支持訪問控制,變量名首字母大寫,才能在全局可用。
局部會覆蓋全局,不同的作用範圍可以重新定義同名變量覆蓋上一級作用域的變量。
package main // 全局變量 var i = 1 func printI() { // 局部變量覆蓋全局變量 i := 10 fmt.Println(i) } func main() { fmt.Println(i) printI() }
局部變量有幾種情況,分別是函數的參數與返回值,函數體內部定義變量,函數內部語法塊等。
函數體內部作用域的例子。
func main() { s := "局部變量" { s := "語法塊內部變量" fmt.Println(s) } fmt.Println(s) }
變量作用域是一個很坑的話題,Go 中每個語法塊,如 func、if、for、select、switch 等,都有一個隱式的作用域。基於它,出現了很多坑死人不償命的面試題。
一個簡單的例子。
func get() int { return 1 } func main() { if x := get(); x == 0 { fmt.Println(x) } else { fmt.Println(x) } }
else 中也可以使用 x 變量,if 之上有個隱式作用域。
作用域這塊還有很多坑,比如與 defer 結合就會產生更多神奇的現象。
面試題
1.1 如下的代碼,哪些能正常編譯?如果不能正常編譯,如何修改?
A.
package main func get() { return 1, 2 } func main() { x, y := get() fmt.Println(x) }
B.
package main var ( x = 1 y := 10 ) func main() { fmt.Println(x) }
C.
package main var i int, s string = 1, "3" func main() { fmt.Println(i, s) }
1.2 下面這段代碼邏輯是否正確?
package main import ( "fmt" ) var p *int func foo() (*int, error) { var i int = 5 return &i, nil } func bar() { // 使用 p fmt.Println(*p) } func main() { p, err := foo() if err != nil { fmt.Println(err) return } bar() fmt.Println(*p) }
注:取自 tonybai 老師的博客,原文地址[1]。
1.3 下面哪一行變量簡短定義存在已定義變量的賦值行為?
package main import ( "fmt" ) func main() { x := 1 fmt.Println(&x) if x, y := 3, 4; true { x = x + y fmt.Println(&x) } x , y := 5, 6 x = x + y fmt.Println(&x) }
參考資料
[1]
原文地址: https://tonybai.com/2015/01/13/a-hole-about-variable-scope-in-golang/