go 學習筆記之值得特別關注的基礎語法有哪些

在上篇文章中,我們動手親自編寫了第一個 Go 語言版本的 Hello World,並且認識了 Go 語言中有意思的變量和不安分的常量.

相信通過上篇文章的斐波那契數列,你已經初步掌握了 Go 語言的變量和常量與其他主要的編程語言的異同,爲了接下來更好的學習和掌握 Go 的基礎語法,下面先簡單回顧一下變量和常量相關知識.

有意思的變量和不安分的常量

  • 變量默認初始化有零值
func TestVariableZeroValue(t *testing.T) {
    var a int
    var s string

    // 0
    t.Log(a, s)
    // 0 ""
    t.Logf("%d %q", a, s)
}
int 類型的變量初始化默認零值是零 0,string 類型的變量默認初始化零值是空字符串 ,其他類型也有相應的零值.
  • 多個變量可以同時賦值
func TestVariableInitialValue(t *testing.T) {
    var a, b int = 1, 2
    var s string = "hello Go"

    // 1 2 hello Go
    t.Log(a, b, s)
}
其他主要的編程語言大多支持多個變量初始化,但極少數有像 Go 語言這樣,不僅支持同時初始化,還可以同時賦值.
  • 多個變量可以用小括號 () 統一定義
func TestVariableShorter(t *testing.T) {
    var (
        a int    = 1
        b int    = 2
        s string = "hello go"
    )

    // 1 2 hello Go
    t.Log(a, b, s)
}
用小括號 () 方式,省略了相同的 var 關鍵字,看起來更加統一
  • 變量類型可以被自動推斷
func TestVariableTypeDeduction(t *testing.T) {
    var a, b, s = 1, 2, "hello Go"

    // 1 2 hello Go
    t.Log(a, b, s)
}
Go 語言可以根據變量值推測出變量類型,所以可以省略變量類型,再一次簡化了變量定義,但是變量類型仍然是強類型,並不像 Js 那樣的弱類型.
  • 變量可以用 := 形式更加簡化
func TestVariableTypeDeductionShorter(t *testing.T) {
    a, b, s := 1, 2, "hello Go"

    // 1 2 hello Go
    t.Log(a, b, s)

    s = "hello golang"

    // 1 2 hello golang
    t.Log(a, b, s)
}
省略了關鍵字 var,轉而使用 := 符號聲明並初始化變量值且利用自動類型推斷能力進一步就簡化變量定義,再次賦值時不能再使用 := 符號.
  • 變量 var 聲明作用域大於變量 := 聲明
var globalTestId = 2
// globalTestName := "type_test" is not supported
var globalTestName = "type_test"

func TestVariableScope(t *testing.T) {
    // 2 type_test
    t.Log(globalTestId, globalTestName)

    globalTestName = "TestVariableScope"

    // 2 TestVariableScope
    t.Log(globalTestId, globalTestName)
}
var 聲明的變量可以作用於函數外或函數內,而 := 聲明的變量只能作用於函數內,Go 並沒有全局變量的概念,變量的作用範圍只是針對包而言.
  • 常量的使用方式和變量一致
func TestConstant(t *testing.T) {
    const a, b = 3, 4
    const s = "hello Go"

    // 3 4 hello Go
    t.Log(a, b, s)
}
常量聲明關鍵字 const,常量和變量的使用方式一致,具備類型推斷能力,也存在多種簡化常量定義的形式.
  • 雖然沒有枚舉類型,但可以用 iota 配合常量來實現枚舉
func TestConstant2Enum(t *testing.T) {
    const (
        java = iota
        golang
        cpp
        python
        javascript
    )
    // 0 1 2 3 4
    t.Log(java, golang,cpp,python,javascript)
}
iota 在一組常量定義中首次出現時,其值爲 0,應用到下一個常量時,其值爲開始自增 1,再次遇到iota 恢復 0 .效果非常像 for 循環中的循環索引 i,明明是常量,偏偏玩出了變量的味道,也是我覺得 iota 不安分的原因.
  • 常量 iota 有妙用,還可以進行位運算
func TestConstantIotaBitCalculate(t *testing.T){
    const (
        Readable = 1 << iota
        Writable
        Executable
    )
    // 0001 0010 0100 即 1 2 4
    t.Log(Readable, Writable, Executable)

    // 0111 即 7,表示可讀,可寫,可執行
    accessCode := 7
    t.Log(accessCode&Readable == Readable, accessCode&Writable == Writable, accessCode&Executable == Executable)
}
定義二進制位最低位爲 1 時表示可讀的,左移一位表示可寫的,左移兩位表示可執行的,按照按位與運算邏輯,目標權限位若擁有可讀權限,此時和可讀常量進行按位與運算之後的結果一定是可讀的,由此可見,iota 非常適合此類操作.

總體來說,Go 語言中的變量很有意思,常量 iota 不那麼安分,從上述歸納總結中不難看出,Go 語言和其他主流的編程語言還是有很大不同的,學習時要側重於這些特殊之處.

如果想要回顧本節知識點,可以關注公衆號[雪之夢技術驛站]找到go 學習筆記之有意思的變量和不安分的常量 這篇文章進行查看.

簡潔的類型中格外關照了複數

在學習 Go 語言中的變量和常量時,雖然沒有特意強調變量或常量的類型,但是大多數編程語言的類型基本都是差不多的,畢竟大家所處的現實世界是一樣的嘛!

光是猜測是不夠的,現在我們要梳理一遍 Go 語言的類型有哪些,和其他主流的編程語言相比有什麼不同?

Go 語言的變量類型大致可以分爲以下幾種:

  • bool
布爾類型 bool,表示真假 true|false
  • (u)int ,(u)int8 , (u)int16, (u)int32,(u)int64,uintptr
int 類型表示整數,雖然不帶位數並不表示沒有位數,32 位操作系統時長度爲 32 位,64 位操作系統時長度爲 64 位.最後一個 uintptr 是指針類型.
  • byte(uint8) ,rune(int32),string
byte 是字節類型,也是 uint8 的別名,而 runeGo 中的字符類型,也是 int32 的別名.
  • float32 ,float64 ,complex64 ,complex128
只有 float 類型表示小數,沒有 double 類型,類型越少對於開發者而言越簡單,不是嗎? complex64=float32+float32 是複數類型,沒錯!就是高中數學書本上的複數,3+4i 那種奇怪的數字!

Go 的類型還是比較簡單的,整數,小數,複數,字節,字符和布爾類型,相同種類的類型沒有繼續細分不同的名稱而是直接根據類型長度進行命名的,這樣是非常直觀的,見名知意,根據數據大小直接選用類型,不費腦!

作爲一種通用的編程語言,Go 內建類型中居然格外關照了複數這種數學概念類型,是一件有意思的事情,是不是意味着 Go 在工程化項目上做得更好?就像 Go 天生支持併發一樣?

既然爲數不多的類型中格外關照了複數類型,那我們簡單使用下複數類型吧,畢竟其他類型和其他主流的編程語言相差不大.

func TestComplex(t *testing.T) {
    c := 3 + 4i

    // 5
    t.Log(cmplx.Abs(c))
}
生命苦短,直接利用變量類型推斷簡化變量聲明,求出複數類型 c 的模(絕對值)

既然學習了複數,怎麼能少得了歐拉公式,畢竟是"世界上最美的公式",剛好用到了複數的相關知識,那我們就簡單驗證一下吧!

go-base-grammar-complex-euler.png

func TestEuler(t *testing.T) {
    // (0+1.2246467991473515e-16i)
    t.Log(cmplx.Pow(math.E, 1i*math.Pi) + 1)

    // (0+1.2246467991473515e-16i)
    t.Log(cmplx.Exp(1i*math.Pi) + 1)

    // (0.000+0.000i)
    t.Logf("%.3f", cmplx.Exp(1i*math.Pi)+1)
}
由於複數 complex 是使用 float 類型表示的,而 float 類型無論是什麼編程語言都是不準確的,所以歐拉公式的計算結果非常非常接近於零,當只保留小數點後三位時,計算結果便是 (0.000+0.000i) ,複數的模也就是 0,至此驗證了歐拉公式.

看過複數還是要研究類型特點

複數很重要,但其他類型也很重要,簡單瞭解過複數的相關知識後,我們仍然要把注意力放到研究這些內建類型的特殊之處上或者說這些類型總體來說相對於其他主流的編程語言有什麼異同.

  • 只有顯示類型轉換,不存在隱式類型轉換
func TestExplicitTypeConvert(t *testing.T) {
    var a, b int = 3, 4
    var c int
    c = int(math.Sqrt(float64(a*a + b*b)))

    // 3 4 5
    t.Log(a, b, c)
}
已知勾股定理的兩條直角邊計算斜邊,根據勾股定理得,直角邊長度的平方和再開根號即斜邊長度,然而 math.Sqrt 方法接收的 float64 類型,返回的也是 float64 類型,可實際值全是 int 類型,這種情況下並不會自動進行類型轉換,只能進行強制類型轉換才能得到我們的期望值,這就是顯示類型轉換.
  • 別名類型和原類型也不能進行隱式類型轉換
func TestImplicitTypeConvert2(t *testing.T) {
    type MyInt64 int64

    var a int64 = 1
    var b MyInt64

    // b = a : cannot use a (type int64) as type MyInt64 in assignment
    b = MyInt64(a)
    t.Log(a, b)
}
MyInt64int64 的別名,別名類型的 b 和原類型的 a 也不能進行也不能進行隱式類型轉換,會報錯 cannot use a (type int64) as type MyInt64 in assignment,只能進行顯示類型轉換.
  • 支持指針類型,但不支持任何形式的計算
func TestPointer(t *testing.T) {
    var a int = 1
    var pa *int = &a

    // 0xc0000921d0 1 1
    t.Log(pa, *pa, a)

    *pa = 2

    // 0xc0000901d0 2 2
    t.Log(pa, *pa, a)
}
同樣的,指針類型也是其他編程語言反過來書寫的,個人覺得這種反而不錯,指向 int 類型的指針 *int,&a 是變量 a 的內存地址,所以變量 pa 存的就是變量 a 的地址,*pa 剛好也就是變量 a 的值.

上例顯示聲明瞭變量類型卻沒有利用到 Go 的類型推斷能力,擺在那的能力卻不利用簡直是浪費,所以提供一種更簡短的方式重寫上述示例,並順便解釋後半句: "指針類型不支持任何形式的計算"


func TestPointerShorter(t *testing.T) {
    a := 1
    pa := &a

    // 0xc0000e6010 1 1
    t.Log(pa, *pa, a)

    *pa = 2

    // 0xc0000e6010 2 2
    t.Log(pa, *pa, a)

    // pa = pa + 1 : invalid operation: pa + 1 (mismatched types *int and int)
    //pa = pa + 1

    // *int int int
    t.Logf("%T %T %T", pa, *pa,a)
}
變量 pa 是指針類型,存儲的是變量的內存地址,只可遠觀而不可褻玩,*pa 就是指針所指向的變量的值,可以進行修改,當然沒問題就像可以重新賦值變量 a 一樣,但是指針 pa 是不可以進行任何形式的運算的,pa = pa + 1 就會報錯 invalid operation.

你猜運算符操作有沒有彩蛋呢

變量和類型還只是孤立的聲明語句,沒有計算不成邏輯,並不是所有的程序都是預定義的變量,Go 的運算符是簡單還是複雜呢,讓我們親自體驗一下!

  • 算術運算符少了 ++i--i
func TestArithmeticOperator(t *testing.T) {
    a := 0
    // 0
    t.Log(a)

    a = a + 1
    // 1
    t.Log(a)

    a = a * 2
    // 2
    t.Log(a)

    a = a % 2
    // 0
    t.Log(a)

    a++
    // 1
    t.Log(a)
}
支持大部分正常的運算符,不支持前置自增,前置自減,這也是好事,再也不會弄錯 i++++i 的運算結果啦,因爲根本不支持 ++i !
  • 比較運算符是否相等有花樣
func TestComparisonOperator(t *testing.T) {
    a, b := 0, 1
    t.Log(a, b)

    // false true true
    t.Log(a > b, a < b, a != b)
}
大於,小於,不等於這種關係很正常,Golang 也沒玩出新花樣,和其他主流的編程語言邏輯一樣,不用特別關心.但是關於比較數組 ==,Go 表示有話要說!

Go 中的數組是可以進行比較的,當待比較的兩個數組的維度和數組元素的個數相同時,兩個數組元素順序一致且相同時,則兩個數組相等,而其他主流的編程語言一般而言比較的都是數組的引用,所以這一點需要特別注意.

func TestCompareArray(t *testing.T) {
    a := [...]int{1, 2, 3}
    //b := [...]int{2, 4}
    c := [...]int{1, 2, 3}
    d := [...]int{1, 2, 4}

    // a == b --> invalid operation: a == b (mismatched types [3]int and [2]int)
    //t.Log(a == b)

    // true false
    t.Log(a == c,a == d)
}
數組 ac 均是一維數組且元素個數都是 3,因此兩個數組可以比較且相等,若數組ab 進行比較,則報錯 invalid operation,是因爲兩個數組的元素個數不相同,無法比較!
  • 邏輯運算符老實本分無異常
func TestLogicalOperator(t *testing.T) {
    a, b := true, false
    t.Log(a, b)

    // false true false true
    t.Log(a&&b,a||b,!a,!b)
}
  • 位運算符新增按位清零 &^ 很巧妙

Go 語言中定義按位清零運算符是 &^,計算規律如下:

當右邊操作位數爲 1 時,左邊操作爲不論是 1 還是 0 ,結果均爲 0;
當右邊操作位數爲 0 時,結果同左邊操作位數.

func TestClearZeroOperator(t *testing.T) {
    // 0 0 1 0
    t.Log(1&^1, 0&^1, 1&^0, 0&^1)
}

不知道還記不記得,在介紹常量 iota 時,曾經以文件權限爲例,判斷給定的權限碼是否擁有特定權限,同樣是給定的權限碼,又該如何撤銷特定權限呢?

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

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

    // 0111 &^ 0001 = 0110 即清除可讀權限
    accessCode = accessCode &^ Readable
    t.Log(accessCode&Readable == Readable, accessCode&Writing == Writing, accessCode&Executable == Executable)
}
accessCode = accessCode &^ Readable 進行按位清零操作後就失去了可讀權限,accessCode&Readable == Readable 再次判斷時就沒有可讀權限了.

流程控制語句也有自己的傲嬌

if 有話要說

有了變量類型和各種運算符的加入,現在實現簡單的語句已經不是問題了,如果再輔助流程控制語句,那麼實現較爲複雜擁有一定邏輯的語句便可更上一層樓.

Go 語言的 if 條件語句和其他主流的編程語言的語義是一樣的,不一樣的是書寫規則和一些細節上有着自己特點.

  • 條件表達式不需要小括號 ()
func TestIfCondition(t *testing.T) {
    for i := 0; i < 10; i++ {
        if i%2 == 0 {
            t.Log(i)
        }
    }
}
Go 語言的各種省略形式使得整體上非常簡潔,但也讓擁有其他主流編程語言的開發者初次接觸時很不習慣,語句結束不用分號 ;,條件表達式不用小括號 () 等等細節,如果不用 IDE 的自動提示功能,這些細節肯定要耗費不少時間.
  • 條件表達式中可以定義變量,只要最後的表達式結果是布爾類型即可
func TestIfConditionMultiReturnValue(t *testing.T) {
    const filename = "test.txt"
    if content, err := ioutil.ReadFile(filename); err != nil {
        t.Log(err)
    } else {
        t.Logf("%s\n", content)
    }
}
Go 語言的函數支持返回多個值,這一點稍後再細說,ioutil.ReadFile 函數返回文件內容和錯誤信息,當存在錯誤信息時 err != nil,輸出錯誤信息,否則輸出文件內容.
  • 條件表達式中定義的變量作用域僅限於當前語句塊

go-base-grammar-if-scope.png

如果嘗試在 if 語句塊外訪問變量 content,則報錯 undefined: content

switch 不甘示弱

同其他主流的編程語言相比,switch 語句最大的特點就是多個 case 不需要 break,Go 會自動進行 break,這一點很人性化.

  • switch 會自動 break,除非使用 fallthrough
func TestSwitchCondition(t *testing.T) {
    switch os := runtime.GOOS; os {
    case "darwin":
        t.Log("Mac")
    case "linux":
        t.Log("Linux")
    case "windows":
        t.Log("Windows")
    default:
        t.Log(os)
    }
}
  • 條件表達式不限制爲常量或整數
其他主流的編程語言中 switch 的條件表達式僅支持有限類型,使用方式存在一定侷限性,Go 語言則不同,這一點變化也是很有意思的,使用 switch 做分支控制時不用擔心變量類型了!
  • case 語言支持多種條件,用逗號 , 分開,邏輯或
func TestSwitchMultiCase(t *testing.T) {
    for i := 0; i < 10; i++ {
        switch i {
        case 0, 2, 4, 6, 8, 10:
            t.Log("Even", i)
        case 1, 3, 5, 7, 9:
            t.Log("odd", i)
        default:
            t.Log("default", i)
        }
    }
}
  • 省略 switch 的條件表達式時,switch 的邏輯和多個 if else 邏輯相同
func TestSwitchCaseCondition(t *testing.T) {
    for i := 0; i < 10; i++ {
        switch {
        case i%2 == 0:
            t.Log("Even", i)
        case i%2 == 1:
            t.Log("odd", i)
        default:
            t.Log("default", i)
        }
    }
}

for 姍姍來遲

最後登場的是 for 循環,一個人完成了其他主流編程語言三個人的工作,Go 語言中既沒有 while 循環也,也沒有 do while 循環,有的只是 for 循環.

  • 循環條件不需要小括號 ()
func TestForLoop(t *testing.T) {
    sum := 0
    for i := 1; i <= 100; i++ {
        sum += i
    }
    // 1+2+3+...+99+100=5050
    t.Log(sum)
}
再一次看到條件表達式不需要小括號 () 應該不會驚訝了吧? if 的條件語句表達式也是類似的,目前爲止,接觸到明確需要小括號的 () 也只有變量或常量定義時省略形式了.
  • 可以省略初始條件
func convert2Binary(n int) string {
    result := ""
    for ; n > 0; n /= 2 {
        lsb := n % 2
        result = strconv.Itoa(lsb) + result
    }
    return result
}

func TestConvert2Binary(t *testing.T) {
    // 1 100 101 1101
    t.Log(
        convert2Binary(1),
        convert2Binary(4),
        convert2Binary(5),
        convert2Binary(13),
    )
}
利用整數相除法,不斷取餘相除,得到給定整數的二進制字符串,這裏就省略了初始條件,只有結束條件和遞增表達式.這種寫法同樣在其他主流的編程語言是沒有的,體現了 Go 設計的簡潔性,這種特性在以後的編程中會越來越多的用到,既然可以省略初始條件,相信你也能猜到可不可以省略其他兩個條件呢?
  • 可以省略初始條件和遞增表達式
func printFile(filename string) {
    if file, err := os.Open(filename); err != nil {
        panic(err)
    } else {
        scanner := bufio.NewScanner(file)
        for scanner.Scan() {
            fmt.Println(scanner.Text())
        }
    }
}

func TestPrintFile(t *testing.T) {
    const filename = "test.txt"
    printFile(filename)
}
打開文件並逐行讀取內容,其中 scanner.Scan() 的返回值類型是 bool,這裏省略了循環的初始條件和遞增表達式,只有循環的終止條件,也順便實現了 while 循環的效果.
  • 初始條件,終止條件和遞增表達式可以全部省略
func forever() {
    for {
        fmt.Println("hello go")
    }
}

func TestForever(t *testing.T) {
    forever()
}
for 循環中沒有任何表達式,意味着這是一個死循環,常用於 Web 請求中監控服務端口,是不是比 while(true) 要更加簡單?

壓軸的一等公民函數隆重登場

雖然沒有特意強制函數,但是示例代碼中全部都是以函數形式給出的,函數是封裝的一種形式,更是 Go 語言的一等公民.

  • 返回值在函數聲明的最後,多個返回值時用小括號 ()
func eval(a, b int, op string) int {
    var result int
    switch op {
    case "+":
        result = a + b
    case "-":
        result = a - b
    case "*":
        result = a * b
    case "/":
        result = a / b
    default:
        panic("unsupported operator: " + op)
    }
    return result
}

func TestEval(t *testing.T) {
    t.Log(
        eval(1, 2, "+"),
        eval(1, 2, "-"),
        eval(1, 2, "*"),
        eval(1, 2, "/"),
        //eval(1, 2, "%"),
    )
}
不論是變量的定義還是函數的定義,Go 總是和其他主流的編程語言相反,個人覺得挺符合思維順序,畢竟都是先有輸入才能輸出,多個輸出當然要統一隔離在一塊了.
  • 可以有零個或一個或多個返回值
func divide(a, b int) (int, int) {
    return a / b, a % b
}

func TestDivide(t *testing.T) {
    // 2 1
    t.Log(divide(5, 2))
}
小學時就知道兩個整數相除,除不盡的情況下還有餘數.只不過編程中商和餘數都是分別計算的,Go 語言支持返回多個結果,終於可以實現小學除法了!
  • 返回多個結果時可以給返回值起名字

func divideReturnName(a, b int) (q, r int) {
    return a / b, a % b
}

func TestDivideReturnName(t *testing.T) {
    q, r := divideReturnName(5, 2)

    // 2 1
    t.Log(q, r)
}
還是整數除法的示例,只不過給返回值起了變量名稱 (q, r int),但這並不影響調用者,某些 IDE 可能會基於次特性自動進行代碼補全,調用者接收時的變量名不一定非要是 q,r .
  • 其他函數可以作爲當前函數的參數
func apply(op func(int, int) int, a, b int) int {
    p := reflect.ValueOf(op).Pointer()
    opName := runtime.FuncForPC(p).Name()

    fmt.Printf("Calling function %s with args (%d,%d)\n", opName, a, b)
    return op(a, b)
}

func pow(a, b int) int {
    return int(math.Pow(float64(a), float64(b)))
}

func TestApply(t *testing.T) {
    // 1
    t.Log(apply(func(a int, b int) int {
        return a % b
    }, 5, 2))

    // 25
    t.Log(apply(pow, 5, 2))
}
apply 函數的第一個參數是 op 函數,第二,第三個參數是 int 類型的 a,b.其中 op 函數也接收兩個 int 參數,返回一個 int 結果,因此 apply 函數的功能就是將 a,b 參數傳遞給 op 函數去執行,這種方式比 switch 固定運算類型要靈活方便!
  • 沒有默認參數,可選參數等複雜概念,只有可變參數列表
func sum(numbers ...int) int {
    result := 0
    for i := range numbers {
        result += numbers[i]
    }
    return result
}

func TestSum(t *testing.T) {
    // 15
    t.Log(sum(1, 2, 3, 4, 5))
}
range 遍歷方式後續再說,這裏可以簡單理解爲其他主流編程語言中的 foreach 循環,一般包括當前循環索引和循環項.

指針類型很方便同時也很簡單

Go 的語言整體上比較簡單,沒有太多花裏胡哨的語法,稍微有點特殊的當屬變量的定義方式了,由於具備類型推斷能力,定義變量的方式有點多,反而覺得選擇困難症,不知道這種情況後續會不會有所改變?

Go 語言的爲數不多的類型中就有指針類型,指針本來是 c 語言的概念,其他主流的編程語言也有類似的概念,可能不叫做指針而是引用,但 Go 語言的發展和 c++ 有一定關係,保留了指針的概念.

但是這並不意味着 Go 語言的指針像 C 語言那樣複雜,相反,Go 語言的指針很方便也很簡單,方便是由於提供我們操作內存地址的方式,簡單是因爲不能對指針做任何運算!

簡單回憶一下指針的基本使用方法:

func TestPointerShorter(t *testing.T) {
    a := 1
    pa := &a

    // 0xc0000e6010 1 1
    t.Log(pa, *pa, a)

    *pa = 2

    // 0xc0000e6010 2 2
    t.Log(pa, *pa, a)

    // pa = pa + 1 : invalid operation: pa + 1 (mismatched types *int and int)
    //pa = pa + 1

    // *int int int
    t.Logf("%T %T %T", pa, *pa,a)
}
& 可以獲取變量的指針類型,* 指向變量,但不可以對指針進行運算,所以指針很簡單!

當指針類型和其他類型和函數一起發生化學反應時,我們可能更加關心參數傳遞問題,其他主流的編程語言可能有值傳遞和引用傳遞兩種方式,Go 語言進行參數傳遞時又是如何表現的呢?

func swapByVal(a, b int) {
    a, b = b, a
}

func TestSwapByVal(t *testing.T) {
    a, b := 3, 4

    swapByVal(a, b)

    // 3 4
    t.Log(a, b)
}
swapByVal 函數內部實現了變量交換的邏輯,但外部函數 TestSwapByVal 調用後變量 a,b 並沒有改變,可見 Go 語言這種參數傳遞是值傳遞而不是引用傳遞.

上面示例中參數傳遞的類型都是普通類型,如果參數是指針類型的話,結果會不會不一樣呢?

func swapByRef(a, b *int) {
    *a, *b = *b, *a
}

func TestSwapByRef(t *testing.T) {
    a, b := 3, 4

    swapByRef(&a, &b)

    // 4 3
    t.Log(a, b)
}
指針類型進行參數傳遞時可以交換變量的值,拷貝的是內存地址,更改內存地址的指向實現了原始變量的交換,參數傳遞的仍然是值類型.

實際上,Go 語言進行參數傳遞的只有值類型一種,這一點不像其他主流的編程語言那樣可能既存在值類型又存在引用類型.

既然是值類型進行參數傳遞,也就意味着參數傳遞時直接拷貝一份變量供函數調用,函數內部如何修改參數並不會影響到調用者的原始數據.

如果只是簡單類型並且不希望參數值被修改,那最好不過,如果希望參數值被修改呢?那隻能像上例那樣傳遞指針類型.

簡單類型不論是傳遞普通類型還是指針類型,變量的拷貝過程不會太耗費內存也不會影響狀態.

如果傳遞的參數本身是比較複雜的類型,仍然進行變量拷貝過程估計就不能滿足特定需求了,可能會設計成出傳遞複雜對象的某種內部指針,不然真的要進行值傳遞,那還怎麼玩?

Go 只有值傳遞一種方式,雖然簡單,但實際中如何使用應該有特殊技巧,以後再具體分析,現在回到交換變量的例子,換一種思路.

func swap(a, b int) (int, int) {
    return b, a
}

func TestSwap(t *testing.T) {
    a, b := 3, 4

    a, b = swap(a, b)

    // 4 3
    t.Log(a, b)
}
利用 Go 函數可以返回多個值特性,返回交換後的變量值,調用者接收時相當於重新賦值,比傳遞指針類型要簡單不少!

基礎語法知識總結和下文預告

剛剛接觸 Go 語言時覺得 Go 的語言很簡單也很特別,和其他主流的編程語言相比,有着自己獨特的想法.

語句結束不用分號 ; 而是直接回車換行,這一點有些不習慣,好在強大的 IDE 可以糾正這些細節.

變量聲明時變量名在前,變量類型在後,可能更加符合大腦思維,但是習慣了先寫變量類型再寫變量名,這確實有一定程度的不方便,後來索性不寫變量類型,自然就沒有問題了.

函數聲明同變量聲明類似,返回值放到了最後部分,並且還可以有多個返回值,經過了變量的洗禮,再熟悉函數的這一特點也就不那麼驚訝了,先輸入後輸出,想一想也有道理,難道其他編程語言的順序都是錯的?

接下來就是語法的細節,比如 if 的條件表達式可以進行變量賦值,switch 表達式可以不用 break,只有 for 循環一種形式等等.

這些細節總體來說比較簡單方便,不用關心細節,放心大膽使用,從而專注於業務邏輯,等到語法不對時,IDE 自然會給出相應的報錯提醒,放心大膽 Go !

本文主要介紹了 Go 的基本語法以及和其他主流的編程語言的異同,你 Get 到了嗎?

下文將開始介紹 Go 的內建容器類型,數組,切片,Map 來一遍!

歡迎大家一起學習交流,如有不當之處,懇請指正,如需完整源碼,請在公衆號[雪之夢技術驛站]留言回覆,感謝你的評論與轉發!

雪之夢技術驛站.png

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