1. 程式人生 > >go 學習筆記之值得特別關注的基礎語法有哪些

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 的模(絕對值)

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

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,輸出錯誤資訊,否則輸出檔案內容.

  • 條件表示式中定義的變數作用域僅限於當前語句塊

如果嘗試在 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 來一遍!

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

相關推薦

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

在上篇文章中,我們動手親自編寫了第一個 Go 語言版本的 Hello World,並且認識了 Go 語言中有意思的變數和不安分的常量. 相信通過上篇文章的斐波那契數列,你已經初步掌握了 Go 語言的變數和常量與其他主要的程式語言的異同,為了接下來更好的學習和掌握 Go 的基礎語法,下面先簡單回顧一下變數和常量

MongoDB 學習筆記 權限管理基礎

顯示 命令 修改 技術 sky 需要 bad 官方 改密碼 權限管理基礎 MongoDB有很多用戶roles,這裏只是簡單列舉下命令的使用,具體的role的含義,請查閱官方文檔。 https://docs.mongodb.com/manual/reference/bui

IP地址和子網劃分學習筆記《IP地址基礎篇》

IP地址 MAC地址 網絡通信 一、IP地址和MAC地址 1、MAC地址 MAC(Media Access Control,介質訪問控制)地址,或稱為物理地址,也叫硬件地址,用來定義網絡設備的位置,MAC地址是網卡出廠時設定的,是固定的(但可以通過在設備管理器中或註冊表等方式修改,同一網段內的MA

Python 學習筆記 Numpy 庫——陣列基礎

1. 初識陣列 import numpy as np a = np.arange(15) a = a.reshape(3, 5) print(a.ndim, a.shape, a.dtype, a.si

Go學習筆記高階資料型別

高階資料型別,僅僅是做個概念認識,等到其他相關知識的學習時,再著重分析。 1 function 將 function 作為資料型別的語言有很多,函數語言程式設計的核心理念。 function 是“第一等公民”,function 與其他資料型別一樣,處於平等地位,可以賦值給

go 學習筆記初識 go 語言

Go 是一種開源程式語言,可以輕鬆構建簡單,可靠,高效的軟體. 摘錄自 github: https://github.com/golang/go,其中官網(國外): https://golang.org 和官網(國內): https://golang.google.cn/ Go 是 Google 公司

go 學習筆記環境搭建

千里之行始於足下,開始 Go 語言學習之旅前,首先要搭建好本地開發環境,然後就可以放心大膽瞎折騰了. Go 的環境安裝和其他語言安

go 學習筆記工作空間

搭建好 Go 的基本環境後,現在可以正式開始 Go 語言的學習之旅,初學時建議在預設的 GOPATH 工作空間規範編寫程式碼,基本目錄結構大概是這個樣子. . |-- bin | `-- hello.exe |-- pkg | `-- windows_amd64 | `-- github.

go 學習筆記走進Goland編輯器

工欲善其事必先利其器,命令列工具雖然能夠在一定程度上滿足基本操作的需求,但實際工作中總不能一直使用命令列工具進行編碼操作吧? 學習 Go 語言同樣如此,為此需要尋找一個強大的 IDE 整合環境幫助我們快速開發,據我所知,市面上比較流行的可能有三個選擇: LiteIDE X : LiteIDE 是一款簡單,開

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

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

go 學習筆記陣列還是切片都沒什麼不一樣

上篇文章中詳細介紹了 Go 的基礎語言,指出了 Go 和其他主流的程式語言的差異性,比較側重於語法細節,相信只要稍加記憶就能輕鬆從已有的程式語言切換到 Go 語言的程式設計習慣中,儘管這種切換可能並不是特別順暢,但多加練習尤其是多多試錯,總是可以慢慢感受 Go 語言之美! 在學習 Go 的內建容器前,同樣的,

go 學習筆記go是不是面嚮物件語言是否支援面對物件程式設計?

面向物件程式設計風格深受廣大開發者喜歡,尤其是以 C++, Java 為典型代表的程式語言大行其道,十分流行! 有意思的是這兩中語言幾乎毫無意外都來源於 C 語言,卻不同於 C 的面向過程程式設計,這種面向物件的程式設計風格給開發者帶來了極大的便利性,解放了勞動,鬆耦合,高內聚也成為設計的標準,從而讓我們

go 學習筆記詳細說一說封裝是怎麼回事

關注公眾號[雪之夢技術驛站]檢視上篇文章 猜猜看go是不是面嚮物件語言?能不能面向物件程式設計? 雖然在上篇文章中,我們通過嘗試性學習探索了 Go 語言中關於面向物件的相關概念,更確切的說是關於封裝的基本概念以及相關實現. 但那還遠遠不夠,不能滿足於一條路,而是應該儘可能地多走幾條路,只有這樣才能為以後可

go 學習筆記是否支援以及如何實現繼承

熟悉面向物件的小夥伴們可能會知道封裝,繼承和多型是最主要的特性,為什麼前輩們會如此看重這三種特性,真的那麼重要嗎? 什麼是封裝 什麼是封裝,封裝有什麼好處以及怎麼實現封裝? 相信大多數小夥伴們都有自己的理解,簡而言之,言而簡之,封裝是遮蔽內部實現細節,僅僅對外暴露出有價值介面. 正如平時工作中使用的電

go 學習筆記萬萬沒想到寵物店竟然催生出面向介面程式設計?

到底是要貓還是要狗 在上篇文章中,我們編撰了一則簡短的小故事用於講解了什麼是面向物件的繼承特性以及 Go 語言是如何實現這種繼承語

go 學習筆記無心插柳柳成蔭的介面和無為而治的空介面

如果你還了解程式設計概念中的介面概念,那麼我建議你最好還是先閱讀上一篇文章.詳情請點選 go 學習筆記之萬萬沒想到寵物店竟然催生出面向介面程式設計? ,否則的話,請自動忽略上文,繼續探索 Go 語言的介面有什麼不同之處. 如無法自動跳轉到公眾號「雪之夢技術驛站」文章,可以點選我的頭像,動動你的小手翻翻歷史文

go 學習筆記僅僅需要一個示例就能講清楚什麼閉包

本篇文章是 Go 語言學習筆記之函數語言程式設計系列文章的第二篇,上一篇介紹了函式基礎,這一篇文章重點介紹函式的重要應用之一: 閉包 空談誤國,實幹興邦,以具體程式碼示例為基礎講解什麼是閉包以及為什麼需要閉包等問題,下面我們沿用上篇文章的示例程式碼開始本文的學習吧! 斐波那契數列是形如 1 1 2 3 5

go 學習筆記10 分鐘簡要理解 go 語言閉包技術

閉包是主流程式語言中的一種通用技術,常常和函數語言程式設計進行強強聯合,本文主要是介紹 Go 語言中什麼是閉包以及怎麼理解閉包. 如果讀者對於 Go 語言的閉包還不是特別清楚的話,可以參考上一篇文章 go 學習筆記之僅僅需要一個示例就能講清楚什麼閉包. 或者也可以直接無視,因為接下來會回顧一下前情概要,現在你

go 學習筆記解讀什麼是defer延遲函式

Go 語言中有個 defer 關鍵字,常用於實現延遲函式來保證關鍵程式碼的最終執行,常言道: "未雨綢繆方可有備無患". 延遲函式就是這麼一種機制,無論程式是正常返回還是異常報錯,只要存在延遲函式都能保證這部分關鍵邏輯最終執行,所以用來做些資源清理等操作再合適不過了. 出入成雙有始有終

Python學習筆記:中文編碼和基礎語法

Python 中文編碼 Python中預設的編碼格式是 ASCII 格式,在沒修改編碼格式時無法正確列印漢字,所以在讀取中文時會報錯。 解決方法為只要在檔案開頭加入 # -- coding: UTF-8 -- 或者 #coding=utf-8 就行了(注意:#coding=utf-8