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

前文

前言

  • 在上文中我們學習了變量的各種概念和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~

參考資料

  • 項目鏈接

  • 作者知乎

  • blog

  • Type inference

  • Rob Pike:Less is exponentially more

  • Type inference for go


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