04 | 程序實體的那些事兒(上)
還記得嗎?Go 語言中的程序實體包括變量、常量、函數、結構體和接口。 Go 語言是靜態類型的編程語言,所以我們在聲明變量或常量的時候,都需要指定它們的類型,或者給予足夠的信息,這樣才可以讓 Go 語言能夠推導出它們的類型。
問題:聲明變量有幾種方式?
package main
import (
"flag"
"fmt"
)
func main() {
var name string // [1]
flag.StringVar(&name, "name", "everyone", "The greeting object.") // [2]
flag.Parse()
fmt.Printf("Hello, %v!\n", name)
}
這是一個很簡單的命令源碼文件,我把它命名為 demo7.go。它是 demo2.go 的微調版。我只是把變量name的聲明和對flag.StringVar函數的調用,都移動到了main函數中,這分別對應代碼中的注釋[1]和[2]。
具體的問題是,除了var name string這種聲明變量name的方式,還有其他方式嗎?你可以選擇性地改動注釋[1]和[2]處的代碼。
典型回答
第一種方式需要先對注釋[2]處的代碼稍作改動,把被調用的函數由flag.StringVar改為flag.String,傳參的列表也需要隨之修改,這是為了[1]和[2]處代碼合並的准備工作。
var name = flag.String("name", "everyone", "The greeting object.")
合並后的代碼看起來更簡潔一些。我把注釋[1]處的代碼中的string去掉了,右邊添加了一個=,然后再拼接上經過修改的[2]處代碼。
注意,flag.String函數返回的結果值的類型是string而不是string。類型string代表的是字符串的指針類型,而不是字符串類型。因此,這里的變量name代表的是一個指向字符串值的指針。
因此,在這種情況下,那個被用來打印內容的函數調用就需要微調一下,把其中的參數name改為*name,即:fmt.Printf("Hello, %v!\n", *name)。
第二種方式與第一種方式非常類似,它基於第一種方式的代碼,賦值符號=右邊的代碼不動,左邊只留下name,再把=變成:=。
name := flag.String("name", "everyone", "The greeting object.")
問題解析
這個問題的基本考點有兩個。一個是你要知道 Go 語言中的類型推斷,以及它在代碼中的基本體現,另一個是短變量聲明的用法。
第一種方式中的代碼在聲明變量name的同時,還為它賦了值,而這時聲明中並沒有顯式指定name的類型。
還記得嗎?之前的變量聲明語句是var name string。這里利用了 Go 語言自身的類型推斷,而省去了對該變量的類型的聲明。
你可以認為,表達式類型就是對表達式進行求值后得到結果的類型。Go 語言中的類型推斷是很簡約的,這也是 Go 語言整體的風格。
它只能用於對變量或常量的初始化,就像上述回答中描述的那樣。對flag.String函數的調用其實就是一個調用表達式,而這個表達式的類型是*string,即字符串的指針類型。
至於第二種方式所用的短變量聲明,實際上就是 Go 語言的類型推斷再加上一點點語法糖。
我們只能在函數體內部使用短變量聲明。在編寫if、for或switch語句的時候,我們經常把它安插在初始化子句中,並用來聲明一些臨時的變量。而相比之下,第一種方式更加通用,它可以被用在任何地方。
知識擴展
1. Go 語言的類型推斷可以帶來哪些好處?
當然,在寫代碼時,我們通過使用 Go 語言的類型推斷,而節省下來的鍵盤敲擊次數幾乎可以忽略不計。但它真正的好處,往往會體現在我們寫代碼之后的那些事情上,比如代碼重構。
為了更好的演示,我們先要做一點准備工作。我們依然通過調用一個函數在聲明name變量的同時為它賦值,但是這個函數不是flag.String,而是由我們自己定義的某個函數,比如叫getTheFlag。
package main
import (
"flag"
"fmt"
)
func main() {
var name = getTheFlag()
flag.Parse()
fmt.Printf("Hello, %v!\n", *name)
}
func getTheFlag() *string {
return flag.String("name", "everyone", "The greeting object.")
}
我們可以用getTheFlag函數包裹(或者說包裝)那個對flag.String函數的調用,並把其結果直接作為getTheFlag函數的結果,結果的類型是*string。
這樣一來,var name =右邊的表達式,可以變為針對getTheFlag函數的調用表達式了。這實際上是對“聲明並賦值name變量的那行代碼”的重構。
我們通常把不改變某個程序與外界的任何交互方式和規則,而只改變其內部實現”的代碼修改方式,叫做對該程序的重構。重構的對象可以是一行代碼、一個函數、一個功能模塊,甚至一個軟件系統。
好了,在准備工作做完之后,你會發現,你可以隨意改變getTheFlag函數的內部實現,及其返回結果的類型,而不用修改main函數中的任何代碼。
這個命令源碼文件依然可以通過編譯,並且構建和運行也都不會有問題。也許你能感覺得到,這是一個關於程序靈活性的質變。
我們不顯式地指定變量name的類型,使得它可以被賦予任何類型的值。也就是說,變量name的類型可以在其初始化時,由其他程序動態地確定。
在你改變getTheFlag函數的結果類型之后,Go 語言的編譯器會在你再次構建該程序的時候,自動地更新變量name的類型。如果你使用過Python或Ruby這種動態類型的編程語言的話,一定會覺得這情景似曾相識。
沒錯,通過這種類型推斷,你可以體驗到動態類型編程語言所帶來的一部分優勢,即程序靈活性的明顯提升。但在那些編程語言中,這種提升可以說是用程序的可維護性和運行效率換來的。
Go 語言是靜態類型的,所以一旦在初始化變量時確定了它的類型,之后就不可能再改變。這就避免了在后面維護程序時的一些問題。另外,請記住,這種類型的確定是在編譯期完成的,因此不會對程序的運行效率產生任何影響。
現在,你應該已經對這個問題有一個比較深刻的理解了。
如果只用一兩句話回答這個問題的話,我想可以是這樣的:Go 語言的類型推斷可以明顯提升程序的靈活性,使得代碼重構變得更加容易,同時又不會給代碼的維護帶來額外負擔(實際上,它恰恰可以避免散彈式的代碼修改),更不會損失程序的運行效率。
2. 變量的重聲明是什么意思?
這涉及了短變量聲明。通過使用它,我們可以對同一個代碼塊中的變量進行重聲明。
既然說到了代碼塊,我先來解釋一下它。在 Go 語言中,代碼塊一般就是一個由花括號括起來的區域,里面可以包含表達式和語句。Go 語言本身以及我們編寫的代碼共同形成了一個非常大的代碼塊,也叫全域代碼塊。
回到變量重聲明的問題上。其含義是對已經聲明過的變量再次聲明。變量重聲明的前提條件如下。
- 由於變量的類型在其初始化時就已經確定了,所以對它再次聲明時賦予的類型必須與其原本的類型相同,否則會產生編譯錯誤。
- 變量的重聲明只可能發生在某一個代碼塊中。如果與當前的變量重名的是外層代碼塊中的變量,那么就是另外一種含義了。
- 變量的重聲明只有在使用短變量聲明時才會發生,否則也無法通過編譯。如果要在此處聲明全新的變量,那么就應該使用包含關鍵字var的聲明語句,但是這時就不能與同一個代碼塊中的任何變量有重名了。
- 被“聲明並賦值”的變量必須是多個,並且其中至少有一個是新的變量。這時我們才可以說對其中的舊變量進行了重聲明。
這樣來看,變量重聲明其實算是一個語法糖(或者叫便利措施)。它允許我們在使用短變量聲明時不用理會被賦值的多個變量中是否包含舊變量。可以想象,如果不這樣會多寫不少代碼。
我把一個簡單的例子寫在了“Golang_Puzzlers”項目的puzzlers/article4/q3包中的 demo9.go 文件中,你可以去看一下。
這其中最重要的兩行代碼如下:
var err error
n, err := io.WriteString(os.Stdout, "Hello, everyone!\n")
我使用短變量聲明對新變量n和舊變量err進行了“聲明並賦值”,這時也是對后者的重聲明。
總結
在本篇中,我們聚焦於最基本的 Go 語言程序實體:變量。並詳細解說了變量聲明和賦值的基本方法,及其背后的重要概念和知識。我們使用關鍵字var和短變量聲明,都可以實現對變量的“聲明並賦值”。
這兩種方式各有千秋,有着各自的特點和適用場景。前者可以被用在任何地方,而后者只能被用在函數或者其他更小的代碼塊中。
不過,通過前者我們無法對已有的變量進行重聲明,也就是說它無法處理新舊變量混在一起的情況。不過它們也有一個很重要的共同點,即:基於類型推斷,Go 語言的類型推斷只應用在了對變量或常量的初始化方面。
思考題
如果與當前的變量重名的是外層代碼塊中的變量,那么這意味着什么?
本作品采用知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議進行許可。
歡迎轉載、使用、重新發布,但務必保留文章署名 鄭子銘 (包含鏈接: http://www.cnblogs.com/MingsonZheng/ ),不得用於商業目的,基於本文修改后的作品務必以相同的許可發布。