說說 Go 中的變量(附粗製濫造面試題)

和其他語言沒有區別,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/