Go語言之程序符號重命名
Go程序源代碼中,關鍵字、接口、類型、常量、變量、方法、函數(shù)、字段、標簽(label)等等的名稱都可以稱為符號。
Go可執(zhí)行程序中,符號表主要包含兩種類型的符號:
- 數(shù)據(jù)對象(Data object)
- 函數(shù)(Function)
一般情況下(不是絕對的),在源代碼編譯為可執(zhí)行程序的過程中,
- 關鍵字、局部變量、標簽會轉(zhuǎn)變?yōu)橹噶?、?shù)據(jù)或者消失,而不再是符號
- 接口、類型、全局常量會被保存為不可變數(shù)據(jù),而不再是符號
- 函數(shù)、方法、全局變量、全局常量會被重命名,保存在符號表中
本文主要總結(jié)了函數(shù)、方法、全局變量在編譯過程中通用的重命名規(guī)則,不討論類似內(nèi)聯(lián)優(yōu)化、閉包、非空接口、編譯器生成等復雜的情況。
規(guī)則
Go 1.18版本之前符號重命名常見規(guī)則列表如下:
- 包名.變量名
- 包名.函數(shù)名
- 包名.函數(shù)名.funcN
- 包名.函數(shù)名.funcN.N
- 包名.類型.函數(shù)名
- 包名.類型.函數(shù)名.funcN
- 包名.類型.函數(shù)名.funcN.N
- 包名.(*類型).函數(shù)名
- 包名.(*類型).函數(shù)名.funcN
- 包名.(*類型).函數(shù)名.funcN.N
- 模塊名/包名.變量名
- 模塊名/包名.函數(shù)名
- 模塊名/包名.函數(shù)名.funcN
- 模塊名/包名.函數(shù)名.funcN.N
- 模塊名/包名.類型.函數(shù)名
- 模塊名/包名.類型.函數(shù)名.funcN
- 模塊名/包名.類型.函數(shù)名.funcN.N
- 模塊名/包名.(*類型).函數(shù)名
- 模塊名/包名.(*類型).函數(shù)名.funcN
- 模塊名/包名.(*類型).函數(shù)名.funcN.N
- 包名.init
- 包名.init.N
- 模塊名/包名.init
- 模塊名/包名.init.N
以上規(guī)則羅列過于詳細,主要是因為包含了過多的匿名函數(shù)命名規(guī)則;本文會縮小分類粒度進行歸納:
- 普通函數(shù)
- 匿名函數(shù)
- 方法
- 全局常量
- 模塊
- 初始化函數(shù)
環(huán)境
- OS : Ubuntu 20.04.2 LTS; x86_64
- Go : go version go1.16.2 linux/amd64
代碼清單
完整代碼已經(jīng)上傳到 Github 倉庫:https://github.com/fooree/go-names
目錄和文件結(jié)構(gòu)如下:
go.mod
- module github.com/fooree/go-names
- go 1.16
main.go
- package main
- import (
- "debug/elf"
- "fmt"
- "github.com/fooree/go-names/internal"
- "github.com/fooree/go-names/internal/foo"
- "github.com/fooree/go-names/internal/foo/ree"
- "os"
- "path/filepath"
- "reflect"
- "sort"
- "strings"
- "time"
- )
- //go:noinline
- func anonymousType() {
- t := reflect.TypeOf(struct {
- Name string
- }{
- Name: "Jack",
- })
- fmt.Printf("name=%s, string=%s, addres=%p\n", t.Name(), t.String(), t)
- }
- func main() {
- anonymousType()
- ree.Run()
- foo.Y.Foo()
- internal.X.Foo()
- name, _ := filepath.Abs(os.Args[0])
- file, err := elf.Open(name)
- if err != nil {
- panic(err)
- }
- defer func() { _ = file.Close() }()
- symbols, err := file.Symbols()
- if err != nil {
- panic(err)
- }
- slice := make([]string, 0, 100)
- for _, symbol := range symbols {
- const module = "github.com/fooree/go-names"
- const name = "main"
- if strings.HasPrefix(symbol.Name, module) || strings.HasPrefix(symbol.Name, name) {
- slice = append(slice, symbol.Name)
- }
- }
- go func() {
- sort.Slice(slice, func(i, j int) bool {
- return slice[i] < slice[j]
- })
- go func() {
- fmt.Println("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")
- }()
- }()
- time.Sleep(time.Second)
- for _, sym := range slice {
- fmt.Println(sym)
- }
- }
internal/a.go
- package internal
- import (
- "fmt"
- "reflect"
- )
- type Foo interface {
- Foo()
- }
- type Int int
- var X Int
- //go:noinline
- func (i *Int) Foo() {
- t := reflect.TypeOf(i)
- go func() {
- fmt.Printf("i am Int, name=%s, string=%s\n", t.Name(), t.String())
- }()
- }
- func init() {
- X = Int(0x123)
- }
- func init() {
- fmt.Println("X =", X)
- }
internal/foo/b.go
- package foo
- import (
- "fmt"
- "reflect"
- )
- type Ree struct {
- Name string
- }
- //go:noinline
- func (r Ree) Foo() {
- anonymousType()
- t := reflect.TypeOf(r)
- fmt.Printf("i am Ree, name=%s, string=%s\n", t.Name(), t.String())
- }
- //go:noinline
- func anonymousType() {
- t := reflect.TypeOf(struct {
- Name string
- }{
- Name: "Jack",
- })
- fmt.Printf("name=%s, string=%s, addres=%p\n", t.Name(), t.String(), t)
- }
- var Y = Ree{"Rose"}
- func init() {
- fmt.Println("Y =",Y)
- }
internal/foo/ree/c.go
- package ree
- import "sort"
- var arr = []int{1, 5, 6, 2, 7, 3, 7, 2}
- func Run() {
- sort.Slice(arr, func(i, j int) bool {
- return arr[i]<arr[j]
- })
- }
查看符號表
編譯以上代碼并執(zhí)行,在執(zhí)行過程中,對可執(zhí)行程序本身進行符號解析,過濾并輸出以上代碼中定義的符號。
普通函數(shù)
在Go語言中,所有的代碼都必須位于某個包(package)中。
在Go語言中,最特殊的一個包名是main,無論它所在的目錄名稱是什么,編譯后該包下的符號都必須以 main.開頭。
在Go語言中,最特殊的一個函數(shù)是main包中的main函數(shù),其編譯之后的符號名稱為main.main,Go運行時將該函數(shù)作為程序的入口。
main包中的其它有名稱的函數(shù),都會被編譯器重命名為“包名.函數(shù)名”的格式,例如main.anonymousType。
匿名函數(shù)
顧名思義,匿名函數(shù)就是沒有名稱的函數(shù)。
函數(shù)中定義的匿名函數(shù),會被重命名為“包名.函數(shù)名.funcN”的格式,其中N是一個可遞增的數(shù)字。
例如,在以上代碼清單中,
main函數(shù)里defer關鍵字后的func() { _ = file.Close() }()是第一個匿名函數(shù),被重命名為main.main.func1。
main函數(shù)里go關鍵字后的func是第二個匿名函數(shù),被重命名為main.main.func2。
main.main.func2函數(shù)里又定義了兩個匿名函數(shù),它們不再被重命名為funcN格式,而是被重命名為funcN.N格式,分別為main.main.func2.1和main.main.func2.2。
方法
專屬于某一個數(shù)據(jù)類型的函數(shù),稱為方法。
方法,只不過是語法層面的一個稱謂而已,其本質(zhì)就是函數(shù);方法的接受者就是其第一個參數(shù),所以方法至少有一個參數(shù)。
在 A Tour of Go (https://tour.golang.org/methods/1) 中,對函數(shù)的定義為:
- A method is a function with a special receiver argument.
方法的定義格式有兩種:
1.接受者為數(shù)據(jù)類型
例如,reflect/value.go源文件中的Elem方法:
- func (v Value) Elem() Value {
- // 此處省略方法代碼
- }
2.接受者為指針類型
例如,reflect/value.go源文件中的Value方法:
- func (it *MapIter) Value() Value {
- // 此處省略方法代碼
- }
通常情況下,以上兩種格式的方法定義,對應的重命名規(guī)則分別如下:
包名.類型.方法名,例如:reflect.Value.Elem
包名.(*類型).方法名,例如:reflect.(*MapIter).Value
實際情況是:編譯過程中的方法重命名規(guī)則要復雜的多。后續(xù)其他的專題文章會逐漸介紹。
方法中如果包含匿名函數(shù),重命名規(guī)則是在其后追加funcN或funcN.N。
全局變量
全局變量的重命名規(guī)則是“包名.變量名”。
例如os/proc.go源文件中定義的Args變量。
包層級
在Go語言中,一個包可以包含和定義其他的包,這是通過子目錄實現(xiàn)的,從而形成了包的層級結(jié)構(gòu)。
如果包存在層級結(jié)構(gòu),則使用“/”進行包名之間的連接,從而實現(xiàn)包的編譯重命名。
例如,io/fs/源碼目錄中定義包名是fs,該包中的變量和函數(shù),在編譯后它們的包名都是“io/fs”。
模塊
模塊(module)是Go語言的依賴管理工具。
一個模塊一般會包含一個或多個包(package)。
模塊中的包、函數(shù)、方法、全局變量、匿名函數(shù)的重命名規(guī)則與以上總結(jié)的規(guī)則一致,只是需要增加前綴“模塊名/”。
例如,文本代碼清單中定義的模塊名稱是github.com/fooree/go-names,模塊中定義的符號重命名如下:
- github.com/fooree/go-names/internal.(*Int).Foo // 方法名
- github.com/fooree/go-names/internal.(*Int).Foo.func1 // 匿名函數(shù)
- github.com/fooree/go-names/internal.X // 全局變量
- github.com/fooree/go-names/internal/foo.Ree.Foo // 方法名
- github.com/fooree/go-names/internal/foo.Y // 全局變量
- github.com/fooree/go-names/internal/foo.anonymousType // 函數(shù)名
- github.com/fooree/go-names/internal/foo/ree.Run // 函數(shù)名
- github.com/fooree/go-names/internal/foo/ree.Run.func1 // 匿名函數(shù)
- github.com/fooree/go-names/internal/foo/ree.arr
初始化函數(shù)
關于初始化函數(shù)的重命名規(guī)則,請閱讀 【Go】初始化函數(shù)。
結(jié)語
本文總結(jié)了一些基本的符號重命名規(guī)則。
本文轉(zhuǎn)載自微信公眾號「Golang In Memory」,可以通過以下二維碼關注。轉(zhuǎn)載本文請聯(lián)系Golang In Memory公眾號。