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

相關文章
相關標籤/搜索