go 學習筆記之有意思的變數和不安分的常量

雪之夢技術驛站發表於2019-08-12

首先希望學習 Go 語言的愛好者至少擁有其他語言的程式設計經驗,如果是完全零基礎的小白使用者,本教程可能並不適合閱讀或嘗試閱讀看看,系列筆記的目標是站在其他語言的角度學習新的語言,理解 Go 語言,進而寫出真正的 Go 程式.

程式語言中一般都有變數和常量的概念,對於學習新語言也是一樣,變數指的是不同程式語言的特殊之處,而常量就是程式語言的共同點.

學習 Go 語言時儘可能站在巨集觀角度上分析變數,而常量可能一笑而過或者程式語言不夠豐富,所謂的常量其實也是變數,不管怎麼樣現在讓我們開始 Go 語言的學習之旅吧,本教程涉及到的原始碼已託管於 github,如需獲取原始碼,請直接訪問 https://github.com/snowdreams1006/learn-go

go-base-grammar-go.png

編寫第一個 Hello World 程式

學習程式語言的第一件事就是編寫出 Hello World,現在讓我們用 Go 語言開發出第一個可執行的命令列程式吧!

環境前提準備可以參考 走進Goland編輯器

新建 main 目錄,並新建 hello_world.go 檔案,其中檔案型別選擇 Simple Application ,編輯器會幫助我們建立 Go 程式骨架.

go-base-grammar-new-go-application.png

首先輸入 fmt 後觸發語法提示選擇 fmt.Println ,然後會自動匯入 fmt 包.

go-base-grammar-go-application-prompt.png

完整內容如下,僅供參考:

package main

import "fmt"

func main() {
    fmt.Println("Hello World")
}

點選左側綠色啟動按鈕,可以直接執行程式或者利用程式自帶的 Terminal 終端選項卡執行程式,當然也可以用外部命令列工具執行程式.

go-base-grammar-go-application-run.png

go run 命令直接執行,而 go build 命令產生可執行檔案,兩種方式都能如願以償輸出 Hello World .

go-base-grammar-go-application-build.png

知識點歸納

Go 應用程式入口的有以下要求:

  • 必須是 main 包 :package main
  • 必須是 main 方法 : func main()
  • 檔名任意不一定是 main.go,目錄名也任意不一定是 main 目錄.

以上規則可以很容易在編輯器中得到驗證,任意一條不符合規則,程式都會報錯提示,這也是使用編輯器而不是命令列進行學習的原因,能夠幫助我們及時發現錯誤,方便隨時驗證猜想.

總結來說,main 包不一定在 main 目錄下,main 方法可以在任意檔案中.

這也意味著程式入口所在的目錄不一定叫做 main 目錄卻一定要宣告為 main 包,雖然不理解為什麼這麼設計,這一點至少和 Java 完全不一樣,至少意味著 Go檔案可以直接遷移目錄而不需要語言層面的重構,可能有點方面,同時也有點疑惑?!

go-base-grammar-main-rule-surprise.png

main 函式值得注意的不同之處:

  • main 函式不支援返回值,但可以通過 os.Exit 返回退出狀態

go-base-grammar-main-rule-return.png

main 函式,不支援返回值,若此時強行執行 main 方法,則會報錯: func main must have no arguments and no return values

go-base-grammar-main-rule-exit.png

main 函式可以藉助 os.Exit(-1) 返回程式退出時狀態碼,外界可以根據不同狀態碼識別相應狀態.

  • main 函式不支援傳入引數,但可以通過 os.Args 獲取引數

go-base-grammar-main-rule-args.png

Terminal 終端選項卡中執行 go run hello_world.go snowdreams1006 命令 os.Args 輸出命令路徑和引數值.

在測試用例中邊學邊練基礎語法

The master has failed more times than the beginner has tried

計算機程式設計不是理科而是工科,動手親自實踐一遍才能更好地掌握知識技能,幸運的是,Go 語言本身內建提供了測試框架,不用載入第三方類庫擴充套件,非常有利於學習練習.

剛剛接觸 Go 語言,暫時不需要深入講解如何編寫規範的測試程式,畢竟基礎語法還沒開始正式練習呢!

但是,簡單的規則還是要說的,總體來說,只有兩條規則:

  • 測試檔名以 _test 結尾 : XXX_test.go

命令習慣和不同, Java 中的檔名一般是大駝峰命名法,相應的測試檔案是 XXXTest

  • 測試方法名以 Test 開頭 : TestXXX

命名習慣和其他程式語言不同,Java 中的測試方法命名是一般是小駝峰命名法,相應的測試方法是 testXXX

  • 測試方法有著固定的引數 : t *testing.T

其他程式語言中一般沒有引數,Java 中的測試方法一定沒有引數,否則丟擲異常 java.lang.Exception: Method testXXX should have no parameters

新建 Go 檔案,型別選擇 Empty File ,檔名命名為 hello_world_test ,編輯器新建一個空白的測試檔案.

go-base-grammar-test-rule-file.png

此時編寫測試方法簽名,利用編輯器自動提示功能輸入 t.Log 隨便輸出些內容,這樣就完成了第一個測試檔案.

go-base-grammar-test-rule-log.png

main 程式一樣,測試方法也是可執行的,編輯器視窗的左側也會出現綠色啟動按鈕,執行測試用例在編輯器下方的控制檯視窗輸出 PASS 證明測試邏輯正確!

go-base-grammar-test-rule-pass.png

測試檔案原始碼示例:

package main

import "testing"

func TestHelloWorld(t *testing.T){
    t.Log("Hello Test")
}

現在已經學習了兩種基本方式,一種是把程式寫在 main 方法中,另一種是把程式寫在測試方法中.

兩種方式都可以隨時測試驗證我們的學習成果,如果寫在 main 方法中,知識點一般要包裝成單獨的方法,然後再在 main 方法中執行該方法.

如果寫在測試方法中,可以單獨執行測試方法,而不必在 main 方法中一次性全部執行.

當然,這兩種方式都可以,只不過個人傾向於測試用例方式.

實現 Fibonacci 數列

形如 1,1,2,3,5,8,13,... 形式的數列就是斐波那契數列,特點是從三個元素開始,下一個元素的值就是前兩兩個元素值的總和,子子孫孫無窮盡也!

記得學習初中歷史時,關於昭君出塞的故事廣為人知,王昭君的美貌不是此次討論的重點,而此次關注點是放到了昭君的悲慘人生.

漢朝和匈奴和親以換取邊境和平,漢朝皇帝不願意自己的親閨女遠嫁塞北,於是從後宮中挑選了一名普通宮女充當和親物件,誰成想這名宮女竟長得如此美貌,"沉魚落雁閉月羞花",堪稱古代中國四大美女之一!

昭君擔負著和親重任,從此開始了遠離他鄉的悲慘生活,一路上,黃沙飛揚,燥熱憂傷,情之所至,昭君拿出隨性的琵琶,演奏出感人淚下的<>!

"千載琵琶作胡語,分明怨恨曲中論",可能情感過於哀傷,竟然連天上的大雁都忘記了飛翔,因此收穫落雁之美!

go-base-grammar-fibonacci-zhaojun.jpg

老單于這個肥波納了個如花似玉的妾,做夢都能了醒吧,遺憾的是,命不久矣!

如此一來,昭君卻滿心歡喜,異族老公死了,使命完成了,應該能回到朝思夢想的大漢朝故土了吧?

命運弄人,匈奴文化,父死子繼,肥波已逝,但還有小肥波啊,放到漢朝倫理綱常來看,都不能叫做近親結婚了簡直是亂倫好嗎!

小肥波+昭君=小小肥波 ,只要昭君不死,而昭君的現任老公不幸先死,那麼小小肥波又會繼續納妾生娃,理論上真的是子子孫孫無窮盡也!

肥波納妾故事可能長成這個樣子:

  • 肥波,昭君,小肥波

昭君的第一任老公: 肥波+昭君=小肥波,此時昭君剛生一個娃

  • 肥波,小肥波,昭君,小小肥波

昭君的第二任老公: 小肥波+昭君=小小肥波,昭君的娃娶了自己的媽?難怪昭君苦楚悲慘,有苦難言,幸運的是,這次昭君沒有生娃,兩個女孩!

  • 肥波,小肥波,小小肥波,昭君

昭君的第三任老公,小小肥波+昭君=小小小肥波 ,兄終弟及,還是亂倫好嗎,這輩分我是算不出來了.

肥波納妾系列,理論上和愚公移山有的一拼,生命不息,子承父業也好,兄終弟及也罷,數量越來越多,肚子越來越大.

以上故事,純屬虛構,昭君出塞是一件偉大的事情,換來了百年和平,值得尊敬.

迴歸正題,下面讓我們用 Go 語言實現斐波那契數列吧!

go-base-grammar-fibonacci-test.png

func TestFib(t *testing.T) {
    var a = 1
    var b = 1

    fmt.Print(a)
    for i := 0; i < 6; i++ {
        fmt.Print(" ", b)

        temp := a
        a = b
        b = temp + b
    }
    fmt.Println()
}

上述簡單示例,展示了變數的基本使用,簡單總結如下:

  • 變數宣告關鍵字用 var ,型別名在前,變數型別在後,其中變數型別可以省略.
// 宣告變數a和變數b
var a = 1
var b = 1

上述變數語法咋一看像是 Js 賦值,嚴格來說其實並不是那樣,上面變數賦值形式只是下面這種的簡化

// 宣告變數a並指定型別為 int,同理宣告變數b並指定型別為int
var a int = 1
var b int = 1

第一種寫法省略了 int 型別,由賦值 1 自動推斷為 int 在一定程度上簡化了書寫,當然這種形式還可以繼續簡化.

// 省略相同的 var,增加一對小括號 (),將變數放到小括號裡面 
var (
    a = 1
    b = 1
)

可能問,還能不能繼續簡化下,畢竟其餘語言的簡化形式可不是那樣的,答案是可以的!

// 連續宣告變數並賦值
var a, b = 1, 1

當然,其餘語言也有類似的寫法,這並不值得驕傲,下面這種形式才是 Go 語言特有的精簡版形式.

// 省略了關鍵字var,賦值符號=改成了:=,表示宣告變數並賦值 
a, b := 1, 1

就問你服不服?一個小小的變數賦值都能玩出五種花樣,厲害了,我的 Go !

  • 變數型別可以省略,由編譯器自動進行型別推斷

go-base-grammar-var-auto-error.png

類似 Js 的書寫習慣,但本質上仍然是強型別,不會進行不同型別的自動轉換,還會說像 Js 的變數嗎?

  • 同一個變數語句可以對不同變數進行同時賦值

仍然以斐波那契數列為例,Go 官網的示例中使用到的就是變數同時賦值的特性,完整程式碼如下:

package main

import "fmt"

// fib returns a function that returns
// successive Fibonacci numbers.
func fib() func() int {
    a, b := 0, 1
    return func() int {
        a, b = b, a+b
        return a
    }
}

func main() {
    f := fib()
    // Function calls are evaluated left-to-right.
    fmt.Println(f(), f(), f(), f(), f())
}

如果對該特性認識不夠清晰,可能覺得這並不是什麼大不了的事情嘛!

實際上,俗話說,沒有對比就沒有傷害,舉一個簡單的例子: 交換變數

func TestExchange(t *testing.T) {
    a, b := 1, 2
    t.Log(a,b)

    a, b = b, a
    t.Log(a,b)

    temp := a
    a = b
    b = temp
    t.Log(a,b)
}

其他語言中如果需要交換兩個變數,一般都是引入第三個臨時變數的寫法,而 Go 實現變數交換則非常簡單清晰,也符合人的思考而不是計算機的思考.

雖然不清楚底層會不會仍然是採用臨時變數交換,但不管怎麼說,使用該特性交換變數確實很方便!

同時對多個變數進行賦值是 Go 特有的語法,其他語言可以同時宣告多個變數但不能同時賦值.

  • 常量同樣很有意思,也有關鍵字宣告 const.

有些程式語言對常量和變數沒有強制規定,常量可以邏輯上被視為不會修改的變數,一般用全大寫字母提示使用者是常量,為了防止常量被修改,有的程式語言可能會有額外的關鍵字進行約束.

幸運的是,Go 語言的常量提供了關鍵字 const 約束,並且禁止重複賦值,這一點很好,簡單方便.

go-base-grammar-const-assign-error.png

func TestConst(t *testing.T) {
    const pi = 3.14
    t.Log(pi)

    // cannot assign to pi
    pi = 2.828
    t.Log(pi)
}

除了語言層面的 const 常量關鍵字,Go 語言還要一個特殊的關鍵字 iota ,常常和常量一起搭配使用!

當設定一些連續常量值或者有一定規律的常量值時,iota 可以幫助我們快速設定.

func TestConstForIota(t *testing.T) {
    const (
        Mon = 1 + iota
        Tue
        Wed
        Thu
        Fri
        Sat
        Sun
    )
    // 1 2 3 4 5 6 7
    t.Log(Mon, Tue, Wed, Thu, Fri, Sat, Sun)
}

大多數程式語言中,星期一代表的數字幾乎都是 0,星期二是 1,以此類推,導致和傳統認識上偏差,為了校準偏差,更加符合國人習慣,因此將星期一代表的數字 0 加一,以此類推,設定初始 iota 後就可以剩餘星期應用該規律,依次 1,2,3,4,5,6,7

如果不使用 iota 的話,可能需要手動進行連續賦值,比較繁瑣,引入了 iota 除了幫助快速設定,還可以進行位元位級別的操作.

func TestConstForIota(t *testing.T) {
    const (
        Readable = 1 << iota
        Writing
        Executable
    )
    // 0001 0010 0100 即 1 2 4
    t.Log(Readable, Writing, Executable)
}

第一位位元位為 1 時,表示檔案可讀,第二位位元位為 1 時,表示可寫,第三位位元位為 1 時,表示可執行,相應的 10 進位制數值依次為 1,2,4 也就是左移一位,左移兩位,左移三位,數學上也可以記成 2^0,2^1,2^2 .

檔案的可讀,可寫,可執行三種狀態代表了檔案的許可權狀態碼,從而實現了檔案的基本許可權操作,常見的許可權碼有 755644.

按位與 & 運算與程式語言無關,"兩位同時為 1 時,按位與的結果才為 1 ,否則結果為 0 ",因此給定許可權碼我們可以很方便判斷該許可權是否擁有可讀,可寫,可執行等許可權.

// 0111 即 7,表示可讀,可寫,可執行
accessCode := 7
t.Log(accessCode&Readable == Readable, accessCode&Writing == Writing, accessCode&Executable == Executable)

// 0110 即 6,表示不可讀,可寫,可執行
accessCode = 6
t.Log(accessCode&Readable == Readable, accessCode&Writing == Writing, accessCode&Executable == Executable)

// 0100 即 4,表示不可讀,不可寫,可執行
accessCode = 4
t.Log(accessCode&Readable == Readable, accessCode&Writing == Writing, accessCode&Executable == Executable)

// 0000 即 0,表示不可讀,不可寫,不可執行
accessCode = 0
t.Log(accessCode&Readable == Readable, accessCode&Writing == Writing, accessCode&Executable == Executable)

accessCode&Readable 表示目標許可權碼和可讀許可權碼進行按位與運算,而可讀許可權碼的二進位制表示值為 0001 ,因此只要目標許可權碼的二進位制表示值第一位是 1 ,按位與的結果肯定是 0001 ,而 0001 又剛好是可讀許可權碼,所以 accessCode&Readable == Readabletrue 就意味著目標許可權碼擁有可讀許可權.

如果目標許可權碼的二進位制位第一個不是 1 而是 0,則 0&1=0 ,(0|1)^0=0,所以按位與運算結果肯定全是 00000,此時 0000 == 0001 比較值 false ,也就是該許可權碼不可讀.

同理可自主分析,accessCode&Writing == Writing 結果 true 則意味著可寫,否則不可寫,accessCode&Executable == Executable 結果 true 意味著可執行,false 意味著不可執行.

熟悉了 iota 的數學計算和位元位計算後,我們趁熱打鐵,用檔案大小單位繼續練習!

func TestConstForIota(t *testing.T) {
    const (
        B = 1 << (10 * iota)
        Kb
        Mb
        Gb
        Tb
        Pb
    )
    // 1 1024 1048576 1073741824 1099511627776 1125899906842624
    t.Log(B, Kb, Mb, Gb, Tb, Pb)

    // 62.9 KB (64,411 位元組)
    size := 64411.0
    t.Log(size, size/Kb)
}

位元組 Byte 與 千位元組 Kilobyte 之間的進位制單位是 1024 ,也就是 2^10 ,剛好可以用 iota 左移 10 位來表示,一次只移動一次,直接乘以 10 就好了!

怎麼樣,iota 是不是很神奇?同時是不是和我一樣也有點小困惑,iota 這貨到底是啥?

go-base-grammar-const-iota-baidu.png

百度翻譯給我們的解釋是,這貨表示"微量",類似英語單詞的 little 一樣,a little 也表示"一點點".

但是 iota 除了表示一點點之外,好像還擁有自增的能力,這可不是 little 這種量詞能夠傳達的意思.

因此,有可能 iota 並不是原始英語含義,說不定是希臘字母的語言,查詢了標準的 24 個希臘字母表以及對應的英語註釋.

大寫 小寫 英文讀音 國際音標 意義
Α α alpha /ˈælfə/ 角度,係數,角加速度
Β β beta /'beitə/ 磁通係數,角度,係數
Γ γ gamma /'gæmə/ 電導係數,角度,比熱容比
Δ δ delta /'deltə/ 變化量,屈光度,一元二次方
Ε ε epsilon /ep'silon/ 對數之基數,介電常數
Ζ ζ zeta /'zi:tə/ 係數,方位角,阻抗,相對粘度
Η η eta /'i:tə/ 遲滯係數,效率
Θ θ theta /'θi:tə/ 溫度,角度
Ι ι ℩ iota /ai'oute/ 微小,一點
Κ κ kappa /'kæpə/ 介質常數,絕熱指數
λ lambda /'læmdə/ 波長,體積,導熱係數
Μ μ mu /mju:/ 磁導係數,微動摩擦系(因)數,流體動力粘度
Ν ν nu /nju:/ 磁阻係數,流體運動粘度,光子頻率
Ξ ξ xi /ksi/ 隨機數,(小)區間內的一個未知特定值
Ο ο omicron /oumaik'rən/ 高階無窮小函式
π pi /pai/ 圓周率,π(n)表示不大於n的質數個數
Ρ ρ rho /rou/ 電阻係數,柱座標和極座標中的極徑,密度
σ ς sigma /'sigmə/ 總和,表面密度,跨導,正應力
Τ τ tau /tau/ 時間常數,切應力
Υ υ upsilon /ju:p'silən/ 位移
Φ φ phi /fai/ 磁通,角,透鏡焦度,熱流量
Χ χ chi /kai/ 統計學中有卡方(χ2)分佈
Ψ ψ psi /psai/ 角速,介質電通量
Ω ω omega /'oumigə/ 歐姆,角速度,交流電的電角度

希臘字母常常用於物理,化學,生物,科學等學科,作為常量或者變數,不同於一般的英語變數或常量的是,希臘字母表示的變數或常量一般具有特定的語義!

因此,iota 應該是希臘字母 I 的英語表示,該變數或者說常量表示微小,一點的含義.

翻譯成自然語言就是,這個符號表示一點點,如果表達改變的含義,那就是在原來基礎上多那麼一點點,如果表達不改變的含義,應該是隻有一點點,僅此而已.

go-base-grammar-const-iota-little.jpg

當然,以上均是個人猜測,更加專業的說法還是應該看下 Go 語言如何定義 iota ,按住 Ctrl 鍵,滑鼠懸停在 iota 上可以點選進入原始碼部分,如下:

// iota is a predeclared identifier representing the untyped integer ordinal
// number of the current const specification in a (usually parenthesized)
// const declaration. It is zero-indexed.
const iota = 0 // Untyped int.

簡短翻譯:

iota 是預定義識別符號,代表當前常量無符號整型序號,是以 0 作為索引的.

上述註釋看起來晦澀難懂,如果是常量那就就安安靜靜當做常量,不行嗎?怎麼從常量定義中還讀出了迴圈變數索引的味道?

為了驗證猜想,仍然以最簡單的星期轉換為例,模擬每一步時的 iota 的值.

const (
    // iota = 0,Mon = 1 + 0 = 1,符合輸出結果 1,此時 iota  = 1,即 iota 自增1
    Mon = 1 + iota
    // iota = 1,Tue = 1 + iota = 1 + 1 = 2,符合輸出結果 2,此時 iota = 2
    Tue
    // iota = 2,Wed = 1 + iota = 1 + 2 = 3,符合輸出結果 3,此時 iota = 3
    Wed
    Thu
    Fri
    Sat
    Sun
)
// 1 2 3 4 5 6 7
t.Log(Mon, Tue, Wed, Thu, Fri, Sat, Sun)

上述猜想中將 iota 當做常量宣告迴圈中的變數 i,每宣告一次,i++,因此僅需要定義迴圈初始條件和迴圈自增變數即可完成迴圈賦值.

const (
    Mon = 1 + iota
    Tue
    Wed
    Thu
    Fri
    Sat
    Sun
)
// 1 2 3 4 5 6 7
t.Log(Mon, Tue, Wed, Thu, Fri, Sat, Sun)

var days [7]int
for i := 0; i < len(days); i++ {
    days[i] = 1 + i
}
// [1 2 3 4 5 6 7]
t.Log(days)

這樣對應是不是覺得 iota 似乎就是迴圈變數的 i,其中 Mon = 1 + iota 就是迴圈初始體,Mon~Sun 有限常量就是迴圈的終止條件,每一個常量就是下一次迴圈.

如果一個例子不足以驗證該猜想的話,那就再來一個!

const (
Readable = 1 << iota
Writing
Executable
)
// 0001 0010 0100 即 1 2 4
t.Log(Readable, Writing, Executable)

var access [3]int
for i := 0; i < len(access); i++ {
    access[i] = 1 << uint(i)
}
// [1 2 4]
t.Log(access)

上述兩個例子已經初步驗證 iota 可能和迴圈變數 i 具有一定的關聯性,還可以進一步接近猜想.

const (
    // iota=0 const=1+0=1 iota=0+1=1
    first = 1 + iota

    // iota=1 const=1+1=2 iota=1+1=2
    second

    // iota=2 const=2+2=4 iota=2+1=3
    third = 2 + iota

    // iota=3 const=2+3=5 iota=3+1=4
    forth

    // iota=4 const=2*4=8 iota=4+1=5
    fifth = 2 * iota

    // iota=5 const=2*5=10 iota=5+1=6
    sixth

    // iota=6 const=6 iota=6+1=7
    seventh = iota
)
// 1 2 4 5 8 10 6
t.Log(first, second, third, forth, fifth, sixth, seventh)

const currentIota  = iota
// 0
t.Log(currentIota)

var rank [7]int
for i := 0; i < len(rank); i++ {
    if i < 2 {
        rank[i] = 1 + i
    } else if i < 4 {
        rank[i] = 2 + i
    } else if i < 6 {
        rank[i] = 2 * i
    } else {
        rank[i] = i
    }
}
// [1 2 3 4 5 6 7]
t.Log(rank)

iota 是一組常量初始化中的迴圈變數索引,當這一組變數全部初始化完畢後,iota 重新開始計算,因此新的變數 currentIota 的值為 0 而不是 7

因此,iota 常常用作一組有規律常量的初始化背後的原因可能就是迴圈變數進行賦值,按照這個思路理解前面關於 iota 的例子暫時是沒有任何問題的,至於這種理解是否準確,有待繼續學習 Go 作進一步驗證,一家之言,僅供參考!

變數和常量的基本小結

  • 變數用 var 關鍵字宣告,常量用 const 關鍵字宣告.
  • 變數宣告並賦值的形式比較多,使用時最好統一一種形式,避免風格不統一.
  • 變數型別具備自動推斷能力,但本質上仍然是強型別,不同型別之間並不會自動轉換.
  • 一組規律的常量可以用 iota 常量進行簡化,可以暫時理解為採用迴圈方式對變數進行賦值,從而轉化成常量的初始化.
  • 變數和常量都具有相似的初始化形式,與其他程式語言不同之處在於一條語句中可以對多個變數進行賦值,一定程度上簡化了程式碼的書寫規則.
  • 任何定義但未使用的變數或常量都是不允許存在的,既然用不著,為何要宣告?!禁止冗餘的設計,好壞暫無法評定,既然如何設計,那就遵守吧!

與眾不同的變數和常量

斐波那契數列是一組無窮的遞增數列,形如 1,1,2,3,5,8,13... 這種從第三個數開始,後面的數總是前兩個數之和的數列就是斐波那契數列.

如果從第三個數開始考慮,那麼前兩個數就是斐波那契數列的起始值,以後的數字都符合既定規律,取前兩個數字當做變數 a,b 採用迴圈的方式不斷向後推進數列得到指定長度的數列.

func TestFib(t *testing.T) {
    var a int = 1
    var b int = 1

    fmt.Print(a)
    for i := 0; i < 6; i++ {
        fmt.Print(" ", b)

        temp := a
        a = b
        b = temp + b
    }
    fmt.Println()
}

雖然上述解法比較清晰明瞭,但還不夠簡潔,至少沒有用到 Go 語言的特性.實際上,我們還可以做得更好,或者說用 Go 語言的特性來實現更加清晰簡單的解法:

func TestFibSimplify(t *testing.T) {
    a, b := 0, 1

    for i := 0; i < 6; i++ {
        fmt.Print(" ", b)
        
        a, b = b, a+b
    }

    fmt.Println()
}

和第一種解法不同的是,這一次將變數 a 向前移一位,人為製造出虛擬頭節點 0,變數 a 的下一個節點 b 指向斐波那契數列的第一個節點 1,隨著 ab 相繼向後推進,下一個迴圈中的節點 b 直接符合規定,相比第一種解法縮短了一個節點.

a, b := 0, 1 是迴圈開始前的初始值,b 是斐波那契數列中的第一個節點,迴圈進行過程中 a, b = b, a+b 語義非常清楚,節點的 a 變成節點 b,節點 ba+b 的值.

是不是很神奇,這裡既沒有用到臨時變數儲存變數 a 的值,也沒有發生變數覆蓋的情況,直接完成了變數的交換賦值操作.

由此可見, a, b = b, a+b 並不是 a=bb=a+b 的執行結果的累加,而是同時完成的,這一點有些神奇,不知道 Go 是如何實現多個變數同時賦值的操作?

如果有小夥伴知道其中奧妙,還望不吝賜教,大家一起學習進步!

如果你覺得上述操作有點不好理解,那麼接下來的操作,相信你一定會很熟悉,那就是兩個變數的值進行交換.

func TestExchange(t *testing.T) {
    a, b := 1, 2
    t.Log(a, b)

    a, b = b, a
    t.Log(a, b)

    temp := a
    a = b
    b = temp
    t.Log(a, b)
}

同樣的是,a, b = b, a 多變數同時賦值直接完成了變數的交換,其他程式語言實現類似需求一般都是採用臨時變數提前儲存變數 a 的值以防止變數覆蓋,然而 Go 語言的實現方式竟然和普通人的思考方式一樣,不得不說,這一點確實不錯!

通過簡單的斐波那契數列,引入了變數和常量的基本使用,以及 Go 的原始碼檔案相應規範,希望能夠帶你入門 Go 語言的基礎,瞭解和其它程式語言有什麼不同以及這些不同之處對我們實際編碼有什麼便捷之處,如果能用熟悉的程式語言實現 Go 語言的設計思想也未曾不是一件有意思的事情.

下面,簡單總結下本文涉及到的主要知識點,雖然是變數和常量,但重點並不在如何介紹定義上,而是側重於特殊之處以及相應的實際應用.

  • 原始碼檔案所在的目錄和原始碼檔案的所在包沒有必然聯絡,即 package main 所在的原始碼檔案並不一定在 main 目錄下,甚至都不一定有 main 目錄.

go-base-grammar-summary-main.png

hello.go 原始碼檔案位於 hello 目錄下,而 hello_word.go 位於 main 目錄下,但是他們所在的包都是 package main

  • 原始碼檔案命名暫時不知道有沒有什麼規則,但測試檔案命名一定是 xxx_test,測試方法命名是 TestXXX ,其中 Go 天生支援測試框架,不用額外載入第三方類庫.

go-base-grammar-summary-test.png

  • 宣告變數的關鍵字是 var,宣告常量的關鍵字是 const,無論是變數還是常量,均存在好幾種宣告方式,更是存在自動型別推斷更可以進行簡化.

go-base-grammar-summary-var.png

一般而言,實現其它程式語言中的全域性變數宣告用 var,區域性變數宣告 := 簡化形式,其中多個變數可以進行同時賦值.

  • 一組特定規律的常量值可以巧用 iota 來實現,可以理解為首次使用 iota 的常量是這組常量規律的第一個,其餘的常量按照該規律依次初始化.

go-base-grammar-summary-iota.png

Go 語言沒有列舉類,可以用一組常量值實現列舉,畢竟列舉也是特殊的常量.

本文原始碼已上傳到 https://github.com/snowdreams1006/learn-go 專案,感興趣的小夥伴可以點選檢視,如果文章中有描述不當之處,懇請指出,謝謝你的評論和轉發.

雪之夢技術驛站

相關文章