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