Go語言核心36講(Go語言基礎知識四)--學習筆記

MingsonZheng 發表於 2021-10-14
Go

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語句的時候,我們經常把它安插在初始化子句中,並用來宣告一些臨時的變數。而相比之下,第一種方式更加通用,它可以被用在任何地方。

image

知識擴充套件

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/ ),不得用於商業目的,基於本文修改後的作品務必以相同的許可釋出。