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

首先希望學習 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 項目,感興趣的小夥伴可以點擊查看,如果文章中有描述不當之處,懇請指出,謝謝你的評論和轉發.

雪之夢技術驛站

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章