golang 快速入門 [8.2]-自動型別推斷的祕密

weishixianglian發表於2020-03-28

前文

前言

  • 在上文中我們學習了變數的各種概念和 go 語言中的型別系統
  • 我們將在本文中學習到:
    • 什麼是自動型別推斷
    • 為什麼需要自動型別推斷
    • go 語言中自動型別推斷的特點與陷阱
    • go 語言在編譯時是如何進行自動型別推斷的

型別推斷 (Type inference)

  • 型別推斷是程式語言在編譯時自動解釋表示式資料型別的能力,通常在函數語言程式設計的語言(例如 Haskell)中存在,型別推斷的優勢主要在於可以省略型別,這使程式設計任務更加容易。
  • 明確的指出變數的型別在程式語言中很常見,編譯器在多大程度上可以做到這一點,因語言而異。例如,某些編譯器可以推斷出值:變數,函式引數和返回值。
  • go 語言作為靜態型別語言在編譯時就需要知道變數的型別

型別推斷的優勢

  • 使編譯器支援諸如型別推斷之類的東西有兩個主要的優勢。首先,如果使用得當,它可以使程式碼更易讀,例如,可以將如下 C ++ 程式碼: vector<int> v; vector<int>::iterator itr = v.iterator(); 變為: vector<int> v; auto itr = v.iterator();
  • 儘管在這裡獲得的收益似乎微不足道,但是如果型別更加複雜,則型別推斷的價值變得顯而易見。在許多情況下,這將使我們減少程式碼中的冗餘資訊。
  • 型別推斷還用於其他功能,Haskell語言可以編寫為: succ x = x + 1
  • 上面的函式中,不管變數 X 是什麼型別,加 1 並返回結果。
  • 儘管如此,顯式的指出型別仍然有效,因為編譯器可以更輕鬆地瞭解程式碼實際應執行的操作,不太可能犯任何錯誤。

go 語言中的型別推斷

如上所述,型別推斷的能力每個語言是不相同的,在 go 語言中根據開發人員的說法,他們的目標是減少在靜態型別語言中發現的混亂情況。他們認為許多像 Java 或 C++ 這樣的語言中的型別系統過於繁瑣。

  • 因此,在設計 Go 時,他們從這些語言中借鑑了一些想法。 這些想法之一是對變數使用簡單的型別推斷,給人以編寫動態型別程式碼的感覺,同時仍然使用靜態型別的好處
  • 如前所述,型別推斷可以涵蓋引數和返回值之類的內容,但是 Go 中沒有
  • 在實踐中,可以通過在宣告新變數或常量時簡單地忽略型別資訊,或使用:=表示法來觸發 Go 中的型別推斷
  • 在 Go 中,以下三個語句是等效的:

    var a int = 10
    var a = 10
    a := 10
    
  • Go 的型別推斷在處理包含識別符號的推斷方面是半完成的。 本質上,編譯器將不允許對從識別符號引用的值進行強制型別轉換,舉幾個例子:

  • 下面這段程式碼正常執行,並且 a 的型別為 float64

    a := 1 + 1.1
    
  • 下面的程式碼仍然正確,a 會被推斷為浮點數,1會變為浮點數與 a 的值相加

    a := 1.1
    b := 1 + a
    
  • 但是,下面程式碼將會錯誤,即 a 的值已被推斷為整數,而 1.1 為浮點數,但是不能將 a 強制轉換為浮點數,相加失敗。編譯器報錯:constant 1.1 truncated to integer

    a := 1
    b := a + 1.1
    
  • 下面的型別會犯相同的錯誤,編譯器提示:,invalid operation: a + b (mismatched types int and float64)

    a := 1
    b := 1.1
    c := a + b
    

詳細的實現說明

  • 在之前的這篇文章中(go 語言如何編譯為機器碼),我們介紹了編譯器執行的過程:詞法分析 => 語法分析 => 型別檢查 => 中間程式碼 => 程式碼優化 => 生成機器碼
  • 編譯階段的程式碼位於go/src/cmd/compile檔案中 #### 詞法分析階段
  • 具體來說,在詞法分析階段,會將賦值右邊的常量解析為一個未定義的型別,型別有如下幾種:顧名思義,其中 ImagLit 代表複數,IntLit 代表整數...
//go/src/cmd/compile/internal/syntax
const (
 IntLit LitKind = iota
 FloatLit
 ImagLit
 RuneLit
 StringLit
)
  • go 語言原始碼採用 UTF-8 的編碼方式,在進行詞法分析時當遇到需要賦值的常量操作時,會逐個的讀取後面常量的 UTF-8 字元。字串的首字元為",數字的首字母為'0'-'9'。實現函式位於:
// go/src/cmd/compile/internal/syntax

func (s *scanner) next() {
...
switch c {
    case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
        s.number(c)
    case '"':
        s.stdString()
    case '`':
        s.rawString()
    ...
  • 因此對於整數、小數等常量的識別就顯得非常簡單。具體來說,一個整數就是全是"0"-"9"的數字。一個浮點數就是字元中有"."號的數字,字串就是首字元為"
  • 下面列出的函式為小數和整數語法分析的具體實現:
// go/src/cmd/compile/internal/syntax
func (s *scanner) number(c rune) {
    s.startLit()

    base := 10        // number base
    prefix := rune(0) // one of 0 (decimal), '0' (0-octal), 'x', 'o', or 'b'
    digsep := 0       // bit 0: digit present, bit 1: '_' present
    invalid := -1     // index of invalid digit in literal, or < 0

    // integer part
    var ds int
    if c != '.' {
        s.kind = IntLit
        if c == '0' {
            c = s.getr()
            switch lower(c) {
            case 'x':
                c = s.getr()
                base, prefix = 16, 'x'
            case 'o':
                c = s.getr()
                base, prefix = 8, 'o'
            case 'b':
                c = s.getr()
                base, prefix = 2, 'b'
            default:
                base, prefix = 8, '0'
                digsep = 1 // leading 0
            }
        }
        c, ds = s.digits(c, base, &invalid)
        digsep |= ds
    }

    // fractional part
    if c == '.' {
        s.kind = FloatLit
        if prefix == 'o' || prefix == 'b' {
            s.error("invalid radix point in " + litname(prefix))
        }
        c, ds = s.digits(s.getr(), base, &invalid)
        digsep |= ds
    }
...
  • 我們以賦值操作a := 333為例, 當完成詞法分析時, 此賦值語句用AssignStmt表示。
AssignStmt struct {
    Op       Operator // 0 means no operation
    Lhs, Rhs Expr     // Rhs == ImplicitOne means Lhs++ (Op == Add) or Lhs-- (Op == Sub)
    simpleStmt
}
  • 其中Op代表操作符,在這裡是賦值操作,Lhs 與 Rhs 分別代表左右兩個表示式,左邊代表了變數a,右邊代表了整數333,此時右邊整數的型別為intLit #### 抽象語法樹階段
  • 接著生成在抽象語法樹 AST 時, 會將詞法分析的AssignStmt解析變為一個ode,Node結構體是對於抽象語法樹中節點的抽象。
type Node struct {
    // Tree structure.
    // Generic recursive walks should follow these fields.
    Left  *Node
    Right *Node
    Ninit Nodes
    Nbody Nodes
    List  Nodes
    Rlist Nodes
    E   interface{} // Opt or Val, see methods below
    ...
  • 仍然是 Left 左節點代表了左邊的變數a,Right 右節點代表了整數333
  • 此時在 E 介面中,Right 右節點會儲存值333,型別為 mpint。mpint 用於儲存整數常量
  • 具體的程式碼如下,如果為 IntLit 型別,轉換為 Mpint 型別,其他型別類似。
  • 但是注意,此時左邊的節點還是沒有任何型別的。
// go/src/cmd/compile/internal/gc
func (p *noder) basicLit(lit *syntax.BasicLit) Val {
    // TODO: Don't try to convert if we had syntax errors (conversions may fail).
    //       Use dummy values so we can continue to compile. Eventually, use a
    //       form of "unknown" literals that are ignored during type-checking so
    //       we can continue type-checking w/o spurious follow-up errors.
    switch s := lit.Value; lit.Kind {
    case syntax.IntLit:
        checkLangCompat(lit)
        x := new(Mpint)
        x.SetString(s)
        return Val{U: x}

    case syntax.FloatLit:
        checkLangCompat(lit)
        x := newMpflt()
        x.SetString(s)
        return Val{U: x}
  • 如下 Mpint 型別的結構,我們可以看到 AST 階段整數儲存通過 math/big.int 進行高精度儲存。
// Mpint represents an integer constant.
type Mpint struct {
    Val  big.Int
    Ovf  bool // set if Val overflowed compiler limit (sticky)
    Rune bool // set if syntax indicates default type rune
}
  • 最後在抽象語法樹進行型別檢查的階段,會完成最終的賦值操作。將右邊常量的型別賦值給左邊變數的型別。
  • 最終具體的函式位於typecheckas,將右邊的型別賦值給左邊
func typecheckas(n *Node) {
...
if n.Left.Name != nil && n.Left.Name.Defn == n && n.Left.Name.Param.Ntype == nil {
        n.Right = defaultlit(n.Right, nil)
        n.Left.Type = n.Right.Type
    }
}
...
  • mpint型別對應的為CTINT標識。如下所示,前一階段不同型別對應不同的標識。最終左邊的變數儲存的型別會變為types.Types[TINT]
func (v Val) Ctype() Ctype {
 switch x := v.U.(type) {
 default:
  Fatalf("unexpected Ctype for %T", v.U)
  panic("unreachable")
 case nil:
  return 0
 case *NilVal:
  return CTNIL
 case bool:
  return CTBOOL
 case *Mpint:
  if x.Rune {
   return CTRUNE
  }
  return CTINT
 case *Mpflt:
  return CTFLT
 case *Mpcplx:
  return CTCPLX
 case string:
  return CTSTR
 }
}
  • types.Types 是一個陣列,儲存了不同標識對應的 go 語言中的實際型別。
var Types [NTYPE]*Type
  • Type是 go 語言中型別的儲存結構,types.Types[TINT]最終代表的型別為int型別。其結構如下:
// A Type represents a Go type.
type Type struct {
    Extra interface{}

    // Width is the width of this Type in bytes.
    Width int64 // valid if Align > 0

    methods    Fields
    allMethods Fields

    Nod  *Node // canonical OTYPE node
    Orig *Type // original type (type literal or predefined type)

    // Cache of composite types, with this type being the element type.
    Cache struct {
        ptr   *Type // *T, or nil
        slice *Type // []T, or nil
    }

    Sym    *Sym  // symbol containing name, for named types
    Vargen int32 // unique name for OTYPE/ONAME

    Etype EType // kind of type
    Align uint8 // the required alignment of this type, in bytes (0 means Width and Align have not yet been computed)

    flags bitset8
}

  • 最後,我們可以用下面的程式碼來驗證型別,輸出結果為:int
a :=  333
fmt.Printf("%T",a)

總結

  • 在本文中,我們介紹了自動型別推斷的內涵以及其意義。同時,我們用例子指出了 go 語言中自動型別推斷的特點。
  • 最後,我們用a:=333為例,介紹了 go 語言在編譯時是如何進行自動型別推斷的。
  • 具體來說,go 語言在編譯時涉及到詞法分析和抽象語法樹階段。對於數字的處理首先採用了 math 包中進行了高精度的處理,接著會轉換為 go 語言中的標準型別,int 或 float64.在本文中沒有對字串等做詳細介紹,留給以後的文章。
  • see you~

參考資料

喜歡本文的朋友歡迎點贊分享~

唯識相鏈啟用微信交流群(Go 與區塊鏈技術)

歡迎加微信:ywj2271840211

更多原創文章乾貨分享,請關注公眾號
  • golang 快速入門 [8.2]-自動型別推斷的祕密
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章