golang 快速入門 [8.2]-自動型別推斷的祕密
前文
- golang 快速入門 [1]-go 語言導論
- golang 快速入門 [2.1]-go 語言開發環境配置-windows
- golang 快速入門 [2.2]-go 語言開發環境配置-macOS
- golang 快速入門 [2.3]-go 語言開發環境配置-linux
- golang 快速入門 [3]-go 語言 helloworld
- golang 快速入門 [4]-go 語言如何編譯為機器碼
- golang 快速入門 [5.1]-go 語言是如何執行的-連結器
- golang 快速入門 [5.2]-go 語言是如何執行的-記憶體概述
- golang 快速入門 [5.3]-go 語言是如何執行的-記憶體分配
- golang 快速入門 [6.1]-整合開發環境-goland 詳解
- golang 快速入門 [6.2]-整合開發環境-emacs 詳解
- golang 快速入門 [7.1]-專案與依賴管理-gopath
- golang 快速入門 [7.2]-北冥神功—go module 絕技
- golang 快速入門 [8.1]-變數型別、宣告賦值、作用域宣告週期與變數記憶體分配
前言
- 在上文中我們學習了變數的各種概念和 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
更多原創文章乾貨分享,請關注公眾號
- 加微信實戰群請加微信(註明:實戰群):gocnio
相關文章
- golang 快速入門 [8.4]-常量與隱式型別轉換Golang型別
- TypeScript 入門自學筆記 — 型別斷言(二)TypeScript筆記型別
- golang快速入門(四)Golang
- TypeScript 型別推斷TypeScript型別
- Java™ 教程(型別推斷)Java型別
- 移動端快速開發的祕密武器
- C#快速入門教程(22)—— 常用集合型別C#型別
- 深入解析C++的auto自動型別推導C++型別
- airtest自動化測試工具快速入門AI
- Appium自動化(9) - appium元素定位的快速入門APP
- c/c++ 模板 型別推斷C++型別
- Java 10新特性:型別推斷Java型別
- C++ auto 型別推斷注意的地方C++型別
- Go快速入門 07 | 集合型別: array、slice 和 map的使用Go型別
- C#快速入門教程(11)—— 字元和字串型別C#字元字串型別
- C#快速入門教程(7)——資料型別概述C#資料型別
- 自學前端如何快速入門?怎麼快速入門前端?前端
- golang 快速入門 [3]-go 語言 helloworldGolang
- Golang快速入門:從菜鳥變大佬Golang
- Golang語言之管道channel快速入門篇Golang
- golang快速入門(六)特有程式結構Golang
- Typescript型別推斷技巧你知道麼?TypeScript型別
- Python 3 快速入門 1 —— 資料型別與變數Python資料型別變數
- 夯實Java基礎系列2:Java基本資料型別,以及自動拆裝箱裡隱藏的祕密Java資料型別
- golang 快速入門 [1]-go 語言導論Golang
- Golang語言檔案操作快速入門篇Golang
- C#快速入門教程(9)——浮點數、Decimal型別和數值型別轉換C#Decimal型別
- golang 快速入門 [8.1]-變數型別、宣告賦值、作用域宣告週期與變數記憶體分配Golang變數型別賦值記憶體
- Qt元物件系統自帶型別與註冊型別的判斷QT物件型別
- 直播app原始碼,登入時自動輸入密碼/自動記住密碼APP原始碼密碼
- golang 快速入門 [8.3]-深入理解浮點數Golang
- golang的型別轉換Golang型別
- [PowerShell] 快速入門, 基本語法, 常用型別, 函式, .NET 互操作型別函式
- golang 入門Golang
- MySQL入門--資料型別MySql資料型別
- 讀取oracle long型別及判斷是否自動分割槽表Oracle型別
- Android Camera 開發你該知道的祕密㊙️-新手入門必備Android
- C#快速入門教程(21)—— 泛型C#泛型