自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Go語言之深入理解函數(shù)

開發(fā) 后端
在計(jì)算機(jī)程序設(shè)計(jì)中,函數(shù)其實(shí)是一種抽象概念,是一種編程接口;通過抽象,能夠?qū)崿F(xiàn)將復(fù)雜的系統(tǒng)分解成各種包裝了復(fù)雜算法的不透明接口,方便彼此相互調(diào)用,實(shí)現(xiàn)分層、擴(kuò)展性、便利性等等。

[[429304]]

概念

在計(jì)算機(jī)程序設(shè)計(jì)中,函數(shù)其實(shí)是一種抽象概念,是一種編程接口;通過抽象,能夠?qū)崿F(xiàn)將復(fù)雜的系統(tǒng)分解成各種包裝了復(fù)雜算法的不透明接口,方便彼此相互調(diào)用,實(shí)現(xiàn)分層、擴(kuò)展性、便利性等等。

具體來講,函數(shù)一般是指一段獨(dú)立的、可重復(fù)利用的程序邏輯片段,用來方便其他函數(shù)調(diào)用;英文名稱是function,有時(shí)候也稱為method、routine。

編譯器最終將函數(shù)編譯為機(jī)器指令,保存在可執(zhí)行文件中。

在進(jìn)程的內(nèi)存空間中,一個(gè)函數(shù)只不過是一段包含機(jī)器指令的連續(xù)內(nèi)存區(qū)域;僅僅從結(jié)構(gòu)上來講,和數(shù)組沒什么區(qū)別。

在Go語言中,函數(shù)(function)是一等公民(first-class citizen),不僅僅是代碼片段,也是一種數(shù)據(jù)類型;和其他數(shù)據(jù)類型一樣有自己的類型信息。

函數(shù)類型

函數(shù)類型的定義有多處,它們是等價(jià)的。

在runtime/type.go源文件中定義如下:

  1. type functype struct { 
  2.     typ      _type 
  3.     inCount  uint16 
  4.     outCount uint16 

在reflect/type.go和internal/reflectlite/type.go源文件中定義如下:

  1. // funcType represents a function type. 
  2. // 
  3. // A *rtype for each in and out parameter is stored in an array that 
  4. // directly follows the funcType (and possibly its uncommonType). So 
  5. // a function type with one method, one input, and one output is
  6. // 
  7. //  struct { 
  8. //    funcType 
  9. //    uncommonType 
  10. //    [2]*rtype    // [0] is in, [1] is out 
  11. //  } 
  12. type funcType struct { 
  13.   rtype 
  14.   inCount  uint16 
  15.   outCount uint16 // top bit is set if last input parameter is ... 

從funcType結(jié)構(gòu)體的注釋中可以看到,函數(shù)類型的信息其實(shí)非常復(fù)雜。

其實(shí)完整的函數(shù)類型定義如下偽代碼所示:

  1. type funcType struct { 
  2.     rtype           // 基礎(chǔ)類型信息  
  3.     inCount  uint16 // 參數(shù)數(shù)量 
  4.     outCount uint16 // 返回值數(shù)量 
  5.     uncommon uncommonType     // 方法信息 
  6.     inTypes  [inCount]*rtype  // 參數(shù)類型列表 
  7.     outTypes [outCount]*rtype // 返回值類型列表 
  8.     methods  [uncommon.mcount]method // 方法列表 

uncommonType和method定義在reflect/type.go源文件中,用于存儲(chǔ)和解析類型的方法信息。

  1. type uncommonType struct { 
  2.     pkgPath nameOff  // 包路徑名稱偏移量 
  3.     mcount  uint16   // 方法的數(shù)量 
  4.     xcount  uint16   // 公共導(dǎo)出方法的數(shù)量 
  5.     moff    uint32   // methods相對(duì)本對(duì)象起始地址的偏移量 
  6.     _       uint32   // unused 
  1. // 非接口類型的方法 
  2. type method struct { 
  3.     name nameOff // 方法名稱偏移量 
  4.     mtyp typeOff // 方法類型偏移量 
  5.     ifn  textOff // 通過接口調(diào)用時(shí)的地址偏移量;接口類型本文不介紹 
  6.     tfn  textOff // 直接類型調(diào)用時(shí)的地址偏移量 
  1. type nameOff int32 // offset to a name 
  2. type typeOff int32 // offset to an *rtype 
  3. type textOff int32 // offset from top of text section 
  • nameOff 是相對(duì) .rodata 節(jié)起始地址的偏移量。
  • typeOff 是相對(duì) .rodata 節(jié)起始地址的偏移量。
  • textOff 是相對(duì) .text 節(jié)起始地址的偏移量。
  • 關(guān)于 reflect.name的介紹,請(qǐng)閱讀 內(nèi)存中的整數(shù) 。

函數(shù)類型結(jié)構(gòu)分布示意圖

完整的函數(shù)類型信息結(jié)構(gòu)分布如下圖所示:

每一種函數(shù)都有自己的類型信息,只不過有的函數(shù)簡(jiǎn)單,有的函數(shù)復(fù)雜,并不是每一種函數(shù)類型包含上圖中的所有字段。

簡(jiǎn)單的函數(shù)類型信息結(jié)構(gòu)分布可能如下圖所示:

或者

備注:以上示意圖中的淺灰色塊表示內(nèi)存對(duì)齊的填充,不存儲(chǔ)任何數(shù)據(jù)。

當(dāng)然,函數(shù)也可能有參數(shù)無返回值,函數(shù)還可能無參數(shù)有返回值,它們的類型信息結(jié)構(gòu)還會(huì)有一點(diǎn)點(diǎn)不同,想象一下,不過只是一種簡(jiǎn)化的結(jié)構(gòu)罷了。

通過本文的內(nèi)存分析,我們將會(huì)了解函數(shù)類型的每一個(gè)細(xì)節(jié)。

環(huán)境

  1. OS : Ubuntu 20.04.2 LTS; x86_64 
  2.  
  3. Go : go version go1.16.2 linux/amd64 

聲明

操作系統(tǒng)、處理器架構(gòu)、Go版本不同,均有可能造成相同的源碼編譯后運(yùn)行時(shí)的寄存器值、內(nèi)存地址、數(shù)據(jù)結(jié)構(gòu)等存在差異。

本文僅包含 64 位系統(tǒng)架構(gòu)下的 64 位可執(zhí)行程序的研究分析。

本文僅保證學(xué)習(xí)過程中的分析數(shù)據(jù)在當(dāng)前環(huán)境下的準(zhǔn)確有效性。

本文僅討論普通函數(shù)和聲明的函數(shù)類型,不討論接口、實(shí)現(xiàn)、閉包等知識(shí)點(diǎn)。

代碼清單

  1. package main 
  2.  
  3. import ( 
  4.   "errors" 
  5.   "fmt" 
  6.   "reflect" 
  7.  
  8. // 聲明函數(shù)類型 
  9. type calc func(a, b int) (sum int
  10.  
  11. // 私有的方法 -> package scope 
  12. //go:noinline 
  13. func (f calc) foo(a, b intint { 
  14.   return f(a, b) + 1 
  15.  
  16. // Ree 公共導(dǎo)出的方法 -> public scope 
  17. //go:noinline 
  18. func (f calc) Ree(a, b intint { 
  19.   return f(a, b) - 1 
  20.  
  21. func main() { 
  22.   // 普通函數(shù) 
  23.   Print(fmt.Printf) 
  24.   // 函數(shù)類型實(shí)例 
  25.   var add calc = func(a, b int) (sum int) { 
  26.     return a + b 
  27.   } 
  28.   fmt.Println(add.foo(1, 2)) 
  29.   fmt.Println(add.Ree(1, 2)) 
  30.   Print(add
  31.   // 匿名函數(shù) 
  32.   Print(func() { 
  33.     fmt.Println("hello anonymous function"
  34.   }) 
  35.   // 方法;閉包 
  36.   f := errors.New("hello error").Error 
  37.   Print(f) 
  38.  
  39.  
  40. //go:noinline 
  41. func Print(i interface{}) { 
  42.   v := reflect.ValueOf(i) 
  43.   fmt.Println("類型", v.Type().String()) 
  44.   fmt.Println("地址", v) 
  45.   fmt.Println() 

運(yùn)行效果

以上代碼清單,主要打印輸出了四個(gè)函數(shù)的類型和內(nèi)存地址。

編譯并運(yùn)行,輸出如下:

在本文的內(nèi)存分析過程中,存在許多通過偏移量計(jì)算內(nèi)存地址的操作。

主要涉及到 .text 和 .rodata 兩個(gè) section,在本程序中它們的信息如下:

普通函數(shù)

以fmt.Printf這個(gè)常用的函數(shù)為例,研究普通函數(shù)的類型信息。

從上面的運(yùn)行輸出結(jié)果可以看到,fmt.Printf函數(shù)類型的字符串表示形式為:

  1. func(string, ...interface {}) (int, error) 

動(dòng)態(tài)調(diào)試

在Print函數(shù)入口處設(shè)置斷點(diǎn),查看fmt.Printf函數(shù)的類型信息。

將fmt.Printf函數(shù)的類型信息繪制成圖表如下:

 

  • rtype.size = 8
  • rtype.ptrdata = 8
  • rtype.hash = 0xd9fb8597
  • rtype.tflag = 2 = reflect.tflagExtraStar
  • rtype.align = 8
  • rtype.fieldAlign = 8
  • rtype.kind = 0x33
  • rtype.equal = 0 = nil
  • rtype.str = 0x00005c90 => *func(string, ...interface {}) (int, error)
  • rtype.ptrToThis = 0
  • funcType.inCount = 2
  • funcType.outCount = 0x8002
  • funcType.inTypes = [ 0x4a4860, 0x4a2f80 ]
  • funcType.outTypes = [ 0x4a41e0, 0x4a9860 ]

指針常量

函數(shù)對(duì)象的大小是8字節(jié)(rtype.size),而且包含8字節(jié)的指針數(shù)據(jù)(rtype.ptrdata),所以我們可以將函數(shù)對(duì)象視為指針。

也就是說fmt.Printf其實(shí)是一個(gè)指針,只不過這個(gè)指針是一個(gè)不可變的常量。這與C/C++是一致的,函數(shù)名稱就是一個(gè)指針常量。

類型名稱

rtype.tflag = 2 = reflect.tflagExtraStar

fmt.Printf函數(shù)有自己的數(shù)據(jù)類型,但是該類型并沒有名稱。

數(shù)據(jù)類別

數(shù)據(jù)類別(Kind)的計(jì)算方法如下:

  1. const kindMask = (1 << 5) - 1 
  2.  
  3. func (t *rtype) Kind() Kind { return Kind(t.kind & kindMask) } 

0x33 & 31 = 19 = reflect.Func

可變參數(shù)

fmt.Printf函數(shù)的參數(shù)數(shù)量(funcType.inCount)是2,返回值數(shù)量也是2,可funcType.outCount值為什么是0x8002?

原因是funcType.outCount字段不但需要記錄函數(shù)返回值的數(shù)量,還需要標(biāo)記函數(shù)最后一個(gè)參數(shù)是否是可變參數(shù)類型;如果是,將funcType.outCount字段值的最高位設(shè)置為1。

在reflect/type.go源文件中,判斷可變參數(shù)的方法如下:

  1. func (t *rtype) IsVariadic() bool { 
  2.     if t.Kind() != Func { 
  3.         panic("reflect: IsVariadic of non-func type " + t.String()) 
  4.     } 
  5.     tt := (*funcType)(unsafe.Pointer(t)) 
  6.     return tt.outCount&(1<<15) != 0 

返回值數(shù)量的計(jì)算方式是:

  1. outCount := funcType.outCount & (1<<15 - 1) 

令人好奇的是,可變參數(shù)標(biāo)記怎么沒有保存在funcType.outCount字段中。

參數(shù)與返回值類型

在fmt.Printf函數(shù)定義中,參數(shù)和返回值的類型依次是:

  • string
  • ...interface{}
  • int
  • error

在內(nèi)存的函數(shù)類型信息中,保存的是參數(shù)和返回值的類型指針;通過這些指針查看它們的類型信息如下:

通過內(nèi)存數(shù)據(jù)可以看到,fmt.Printf函數(shù)的參數(shù)和返回值的數(shù)據(jù)類別(Kind)如下:

  • reflect.String
  • reflect.Slice
  • reflect.Int
  • reflect.Interface

關(guān)于整數(shù)及其類型的詳細(xì)介紹,請(qǐng)閱讀 內(nèi)存中的整數(shù) 。

關(guān)于字符串及其類型的詳細(xì)介紹,請(qǐng)閱讀 內(nèi)存中的字符串 。

在Go語言中,error比較特殊,它既是一個(gè)關(guān)鍵字,又是一個(gè)接口定義。關(guān)于接口類型,之后將發(fā)布專題文章進(jìn)行深入解析,暫不介紹。

關(guān)于slice,內(nèi)存中的slice 一文曾對(duì) []int 進(jìn)行了詳細(xì)介紹 。

很明顯,fmt.Printf函數(shù)的第二個(gè)參數(shù)不是[]int,通過內(nèi)存數(shù)據(jù)來看一看具體是什么類型的slice。

通過上圖可以看到,編譯器將源碼中的可變參數(shù)類型...interface{}編譯為[]interface {},從而把可變參數(shù)變成一個(gè)參數(shù)。

這種處理可變參數(shù)的方式,和Java語言非常相似。

通過對(duì)fmt.Printf函數(shù)的類型深入分析和了解,我們就很容易理解反射包(reflect)中函數(shù)相關(guān)的接口了;有興趣的話可以去看一看源碼實(shí)現(xiàn),相信對(duì)比fmt.Printf函數(shù)的類型信息,是比較簡(jiǎn)單的。

  1. type Type interface { 
  2.     ...... // 省略無關(guān)接口 
  3.     IsVariadic() bool 
  4.     NumIn() int 
  5.     NumOut() int 
  6.     In(i int) Type 
  7.     Out(i int) Type 
  8.     ...... // 省略無關(guān)接口 

聲明的函數(shù)類型

在Go語言中,通過type關(guān)鍵字可以定義任何數(shù)據(jù)類型,非常非常地強(qiáng)悍。

在本文的代碼清單中,我們就使用type關(guān)鍵字定義了calc類型,這明顯是一個(gè)函數(shù)類型。

type calc func (a, b int) (sum int)

這種類型與fmt.Printf函數(shù)類型有什么區(qū)別嗎?使用上述相同的方法,我們來深入研究下。

動(dòng)態(tài)調(diào)試

從內(nèi)存數(shù)據(jù)可以看出,calc類型的add變量指向一個(gè)匿名函數(shù),該匿名函數(shù)被編譯器命名為main.main.func1。

calc的類型信息非常復(fù)雜,共128個(gè)字節(jié),整理成圖表如下:

  • rtype.size = 8
  • rtype.ptrdata = 8
  • rtype.hash = 0x405feca1
  • rtype.tflag = 7 = reflect.tflagUncommon | reflect.tflagExtraStar | reflect.tflagNamed
  • rtype.align = 8
  • rtype.fieldAlign = 8
  • rtype.kind = 0x33
  • rtype.equal = 0 = nil
  • rtype.str = 0x00002253 => *main.calc
  • rtype.ptrToThis = 0x0000ec60
  • funcType.inCount = 2
  • funcType.outCount = 1
  • funcType.inTypes = [ 0x4a41e0, 0x4a41e0 ]
  • funcType.outTypes = [ 0x4a41e0 ]
  • uncommonType.pkgPath = 0x0000034c => main
  • uncommonType.mcount = 2
  • uncommonType.xcount = 1
  • uncommonType.moff = 0x48
  • method[0].name = 0x000001a8 => Ree
  • method[0].mtyp = 0xffffffff
  • method[0].ifn = 0x00098240
  • method[0].tfn = 0x00098240
  • method[1].name = 0x000001f6 => foo
  • method[1].mtyp = 0xffffffff
  • method[1].ifn = 0x000981e0
  • method[1].tfn = 0x000981e0

類型名稱

rtype.tflag字段包含reflect.tflagNamed標(biāo)記,表示該類型是有名稱的。

calc類型的名稱為calc,獲取方式定義在reflect/type.go源文件中:

  1. func (t *rtype) hasName() bool { 
  2.     return t.tflag&tflagNamed != 0 
  3.  
  4. func (t *rtype) Name() string { 
  5.     if !t.hasName() { 
  6.         return "" 
  7.     } 
  8.     s := t.String() 
  9.     i := len(s) - 1 
  10.     for i >= 0 && s[i] != '.' { 
  11.         i-- 
  12.     } 
  13.     return s[i+1:] 
  14.  
  15. func (t *rtype) String() string { 
  16.     s := t.nameOff(t.str).name() 
  17.     if t.tflag&tflagExtraStar != 0 { 
  18.         return s[1:] 
  19.     } 
  20.     return s 

類型指針

  1. rtype.ptrToThis = 0x0000ec60 

該值是相對(duì)程序 .rodata section 的偏移量。在本程序中,.rodata section 的起始地址是 0x49a000。

calc類型的指針類型為*calc,類型信息保存在地址 0x49a000+0x0000ec60處。關(guān)于指針類型本文不再進(jìn)一步介紹。

參數(shù)和返回值

calc類型有2個(gè)參數(shù)和1個(gè)返回值,而且都是int類型(信息保存在0x4a41e0地址處)。

類型方法

方法本質(zhì)上就是函數(shù)。

在 A Tour of Go (https://tour.golang.org/methods/1) 中,對(duì)函數(shù)的定義為:

  1. A method is a function with a special receiver argument. 

calc是函數(shù)類型,函數(shù)類型居然能擁有自己的方法,確實(shí)是巧妙的設(shè)計(jì)。

calc類型的rtype.tflag字段包含reflect.tflagUncommon標(biāo)記,表示其類型信息中包含uncommonType數(shù)據(jù)。

uncommonType對(duì)象的大小是 16 字節(jié),calc類型共有3個(gè)參數(shù)和返回值,3個(gè)類型指針占 24 個(gè)字節(jié),所以 [mcount]method 相對(duì)uncommonType 對(duì)象的偏移是 16 + 24 = 40 字節(jié)。

通過計(jì)算得到如下結(jié)果:

calc類型的Ree方法,被重命名為main.calc.Ree,內(nèi)存地址是0x00098240 + 0x401000 = 0x499240。它是一個(gè)導(dǎo)出函數(shù),所以reflect.name.bytes[0] = 1。

calc類型的foo方法,被重命名為main.calc.foo,內(nèi)存地址是0x000981e0 + 0x401000 = 0x4991e0。

從內(nèi)存分析結(jié)果可以看到,如果一種數(shù)據(jù)類型定義了多個(gè)方法,而且有的是名稱以大寫字母開頭公共導(dǎo)出方法,有的是名稱以小寫字母開頭導(dǎo)私有方法,那么編譯器將公共的導(dǎo)出方法信息排序在前,私有方法信息排序在后,然后保存其數(shù)據(jù)類型信息中。而且這個(gè)結(jié)論可以在reflect/type.go源碼文件中定義的兩個(gè)方法得到印證:

  1. func (t *uncommonType) methods() []method { 
  2.   if t.mcount == 0 { 
  3.     return nil 
  4.   } 
  5.   return (*[1 << 16]method)(add(unsafe.Pointer(t), uintptr(t.moff), "t.mcount > 0"))[:t.mcount:t.mcount] 
  6.  
  7. func (t *uncommonType) exportedMethods() []method { 
  8.   if t.xcount == 0 { 
  9.     return nil 
  10.   } 
  11.   return (*[1 << 16]method)(add(unsafe.Pointer(t), uintptr(t.moff), "t.xcount > 0"))[:t.xcount:t.xcount] 

在本例中還可以看到,無論是Ree方法,還是foo方法,它們對(duì)應(yīng)的method.mtyp字段值都是0xffffffff,也就是 -1。

從runtime/type.go源文件中resolveTypeOff函數(shù)的注釋可以了解到,-1表示沒有對(duì)應(yīng)的類型信息。

也就是說,calc類型的Ree和foo方法雖然也是函數(shù),但是他們沒有對(duì)應(yīng)的函數(shù)類型信息。

所以,Go編譯器并沒有為每一個(gè)函數(shù)都生成對(duì)應(yīng)的類型信息,只是在需要的時(shí)候才會(huì)生成,或者是運(yùn)行時(shí)(runtime)根據(jù)需要生成。

匿名函數(shù)

代碼清單中,第三次調(diào)用main.Print函數(shù)輸出了一個(gè)匿名函數(shù)的類型信息。這個(gè)匿名函數(shù)沒有形成閉包,所以相對(duì)比較簡(jiǎn)單。

將其內(nèi)存數(shù)據(jù)整理成圖表如下:

該函數(shù)沒有參數(shù)、返回值、方法,所以其類型信息非常非常的簡(jiǎn)單。相信已經(jīng)不需要進(jìn)一步介紹了。

總結(jié)

 

通過一步步的內(nèi)存分析,對(duì)Go語言的函數(shù)進(jìn)行了深入的了解,學(xué)習(xí)了很多知識(shí),解開了許多疑惑,相信在實(shí)際開發(fā)中必定能游刃有余,避免一些小坑。

 

責(zé)任編輯:武曉燕 來源: Golang In Memory
相關(guān)推薦

2022-11-07 18:12:54

Go語言函數(shù)

2023-10-27 11:27:14

Go函數(shù)

2021-04-20 23:25:16

執(zhí)行函數(shù)變量

2024-04-07 00:04:00

Go語言Map

2019-11-05 10:03:08

callback回調(diào)函數(shù)javascript

2025-01-13 13:00:00

Go網(wǎng)絡(luò)框架nbio

2019-08-19 12:50:00

Go垃圾回收前端

2020-12-16 09:47:01

JavaScript箭頭函數(shù)開發(fā)

2010-06-01 15:25:27

JavaCLASSPATH

2016-12-08 15:36:59

HashMap數(shù)據(jù)結(jié)構(gòu)hash函數(shù)

2020-07-21 08:26:08

SpringSecurity過濾器

2010-06-28 10:12:01

PHP匿名函數(shù)

2009-11-18 12:38:04

PHP字符串函數(shù)

2021-12-28 17:39:05

Go精度Json

2013-09-22 14:57:19

AtWood

2023-10-19 11:12:15

Netty代碼

2021-02-17 11:25:33

前端JavaScriptthis

2009-09-25 09:14:35

Hibernate日志

2020-09-23 10:00:26

Redis數(shù)據(jù)庫(kù)命令

2017-01-10 08:48:21

點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)