Golang常用語法糖

2023-05-12 09:00:38

1、名字由來

語法糖(Syntactic sugar)的概念是由英國電腦科學家彼得·蘭丁提出的,用於表示程式語言中的某種型別的語法,這些語法不會影響功能,但使用起來卻很方便。
語法糖,也稱糖語法,這些語法不僅不會影響功能,編譯後的結果跟不使用語法糖也一樣。
語法糖,有可能讓程式碼編寫變得簡單,也有可能讓程式碼可讀性更高,但有時也會給你一個意外,也可能帶你掉入陷阱讓您的程式碼出問題。本文將講解Golang常用語法糖。

2、Golang常用語法糖

2.1 簡短變數宣告 :=

規則:簡短變數宣告符這個語法糖使用起來很方便,導致你可能隨手就會使用它定義一個變數,往往程式的bug就是隨手寫出來的,在這裡說一下簡短變數宣告的原理和規則。

(1)多變數賦值可能會重新宣告

使用 := 一次可以宣告多個變數,例如:

i, j := 0, 0
j, k := 1, 1
  • 當 := 左側存在新的變數時(如 k),那麼已經宣告的變數(如 j)會被重新宣告。這並沒有引入新的變數,只是把變數的值改變了。
  • 當 := 左側沒有新變數編譯報錯。如下範例由於左側沒有新變數編譯會提示" No new variables on the left side of ':=' "錯誤。
i,j := 2,3
i,j := 6,8

(2)不能用於函數外部

  • := 這種簡短變數宣告只能用於函數中,用來初始化全域性變數是不行的。

  • 可以理解成 := 會拆分成兩個語句,即宣告和賦值。賦值語句不能出現在函數外部的,因為在任何函數外,語句都應該以關鍵字開頭,例如 type、var這樣的關鍵字。

比如,像下面這樣:

package sugar
import fmt

rule := "Short variable declarations" // syntax error: non-declaration statement outside function body

這是因為在函數外部宣告的變數是全域性變數,它們具有包級別的作用域。在包級別作用域中,變數的宣告通常是顯式的,不需要使用短變數宣告語法糖。而且在全域性變數的宣告中,必須指定變數的型別,這是因為編譯器需要知道變數的大小和佈局資訊,以便在編譯時為它們分配記憶體。

因此,如果要在包級別宣告變數,需要使用 var 關鍵字或 const 關鍵字進行顯式宣告,不能使用 := 語法糖。例如:

package main

import "fmt"

// 使用 var 關鍵字顯式宣告全域性變數
var globalVar = 10

func main() {
    // 在函數內部使用 := 語法糖宣告區域性變數
    localVar := 20
    fmt.Println(globalVar, localVar)
}

總之,:= 只能用於區域性變數的宣告和初始化,而不能用於全域性變數的宣告和初始化,這是 Go 語言的語法規定。

(3)變數作用域問題

幾乎所有的工程師都瞭解變數作用域,但是由於:=使用過於頻繁的話,還是有可能掉進陷阱裡。

下面程式碼源自真實專案,但為了描述方便,也為了避免資訊保安風險,簡化如下:

func Redeclare() {
    field, err:= nextField()   // 1號err

    if field == 1{
        field, err:= nextField()     // 2號err
        newField, err := nextField() //  3號err
        ...
    }
    ...
}

注意上面宣告的三個err變數。 2號err與1號err不屬於同一個作用域,:=宣告了新的變數,所以2號err與1號err屬於兩個變數。 2號err與3號err屬於同一個作用域,:=重新宣告了err但沒建立新的變數,所以2號err與3號err是同一個變數。(同一變數重複賦值會重新宣告,這並沒有引入新的變數,只是把變數的值改變了。)

如果誤把2號err與1號err混淆,就很容易產生意想不到的錯誤。

2.2 可變參函數 ...

我們先寫一個可變參函數:

func Greeting(prefix string, who ...string) {
    if who == nil {
        fmt.Printf("Nobody to say hi.")
        return
    }

    for _, people := range who{
        fmt.Printf("%s %s\n", prefix, people)
    }
}

Greeting函數負責給指定的人打招呼,其引數who為可變引數。這個函數幾乎把可變參函數的特徵全部表現出來了:

  • 可變引數必須在函數參數列的最後一個(否則會引起編譯時歧義);

  • 可變引數在函數內部是作為切片來解析的;

  • 可變引數可以不填,不填時函數內部當成 nil 切片處理;

  • 可變引數可以填入切片;

  • 可變引數必須是相同型別的(如果需要是不同型別的可以定義為 interface{}型別);

(1)使用舉例-不傳值

呼叫可變參函數時,可變參部分是可以不傳值的,例如:

func ExampleGreetingWithoutParameter() {
    sugar.Greeting("nobody")
    // OutPut:
    // Nobody to say hi.
}

這裡沒有傳遞第二個引數。可變引數不傳遞的話,預設為nil。

(2)使用舉例-傳遞多個引數

呼叫可變參函數時,可變引數部分可以傳遞多個值,例如:

func ExampleGreetingWithParameter() {
    sugar.Greeting("hello:", "Joe", "Anna", "Eileen")
    // OutPut:
    // hello: Joe
    // hello: Anna
    // hello: Eileen
}

可變引數可以有多個。多個引數將會生成一個切片傳入,函數內部按照切片來處理。

(3)使用舉例-傳遞切片

呼叫可變參函數時,可變引數部分可以直接傳遞一個切片。引數部分需要使用slice...來表示切片。例如:

func ExampleGreetingWithSlice() {
    guest := []string{"Joe", "Anna", "Eileen"}
    sugar.Greeting("hello:", guest...)
    // OutPut:
    // hello: Joe
    // hello: Anna
    // hello: Eileen
}

此時需要注意的一點是,切片傳入時不會生成新的切片,也就是說函數內部使用的切片與傳入的切片共用相同的儲存空間。說得再直白一點就是,如果函數內部修改了切片,可能會影響外部呼叫的函數。

2.3 new函數

在 Go 語言中,new 函數用於動態地分配記憶體,返回一個指向新分配的零值的指標。它的語法如下:

func new(Type) *Type

其中,Type 表示要分配的記憶體的型別,new 函數返回一個指向 Type 型別的新分配的零值的指標。但是需要注意的是,new 函數只分配記憶體,並返回指向新分配的零值的指標,而不會初始化該記憶體。

所謂零值,是指 Go 語言中變數在宣告時自動賦予的預設值。對於基本型別來說,它們的零值如下:

  • 布林型:false
  • 整型:0
  • 浮點型:0.0
  • 複數型:0 + 0i
  • 字串:""(空字串)
  • 指標:nil
  • 介面:nil
  • 切片、對映和通道:nil

因此,new 函數返回的指標指向新分配的零值,但不會將其初始化為非零值。如果需要將記憶體初始化為非零值,可以使用結構體字面量或者顯式地為其賦值。例如:

package main

import "fmt"

type Person struct {
	name string
	age  int
	sex  int
}

func main() {
	// 使用 new 函數分配記憶體,但不會將其初始化為非零值
	p := new(Person)
	fmt.Println(p) // 輸出:&{ 0 0}

	// 使用結構體字面量初始化
	p2 := &Person{name: "Tom", age: 18, sex: 1}
	fmt.Println(p2) // 輸出:&{Tom 18 1}

	// 顯式為欄位賦值
	p3 := new(Person)
	p3.name = "Jerry"
	p3.age = 20
	p3.sex = 0
	fmt.Println(p3) // 輸出:&{Jerry 20 0}
}

上面的程式碼中,使用 new 函數分配了一個新的 Person 結構體,但不會將其初始化為非零值,因此輸出結果是"空字串 0 0"。接下來,使用結構體字面量或者顯式為其賦值,將其初始化為非零值。  

注意 1:p3 := new(Person) 返回是指向新分配的Person型別物件零值的指標,按照我們對指標語法的瞭解,基於p3顯示賦值的話需要使用如下語法進行賦值:

(*p3).name = "Jerry"
(*p3).age = 20
(*p3).sex = 0

 而我們在對指標型別結構體物件賦值的時候一般都很少會帶著*,這也是Go指標語法糖為我們做的簡化,這部分在後文會詳細介紹。  

注意 2:new函數更多細節介紹,請參見《Go語言new( )函數》這篇博文。

 很明顯,new函數的設計同樣是為了方便程式設計師的使用。 

 參考:https://books.studygolang.com/GoExpertProgramming/chapter10/