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

說說 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/


分享到:


相關文章: