Golang 基礎之函式使用 (三)

帽兒山的槍手發表於2022-05-19

大家好,今天將梳理出的 Go語言函式用法內容,分享給大家。 請多多指教,謝謝。

本次《Go語言函式使用》內容共分為三個章節,本文為第三章節。

本章節內容

  • init 函式
  • 方法

init 函式

介紹

例如某些場景下,我們需要提前初始化一些變數或邏輯程式碼。在這種情況下,我們可以用一個特殊的init初始化函式來簡化初始化工作,每個檔案都可以包含一個或多個init初始化函式。

func init() {} // init()函式語法

init初始化函式除了不能被呼叫或引用外,其他行為和普通函式類似。在每個檔案中的init初始化函式,在程式開始執行時按照它們宣告的順序被自動呼叫。

init函式先於main函式執行

注意:每個包在解決依賴的前提下,以匯入宣告的順序初始化,每個包只會被初始化一次。因此,如果一個p包匯入了q包,那麼在p包初始化的時候可以認為q包必然已經初始化過了。

init函式的特徵

  • init函式是用於程式執行前做包的初始化的函式,比如初始化包裡的變數等
  • init函式沒有輸入引數、返回值
  • 每個包可以擁有多個init函式
  • 包的每個原始檔也可以擁有多個init函式
  • 同一個包中多個init函式的執行順序go語言沒有明確的定義(說明)
  • 不同包的init函式按照包匯入的依賴關係決定該初始化函式的執行順序
  • init函式不能被其他函式呼叫,而是在main函式執行之前,自動被呼叫

初始化的過程

  1. 初始化匯入的包(順序並不是按匯入順序(從上到下)執行的,runtime需要解析包依賴關係,沒有依賴的包最先初始化);
  2. 初始化包作用域的變數(並非按照“從上到下、從左到右”的順序,runtime解析變數依賴關係,沒有依賴的變數最先初始化);
  3. 執行包的init函式;
runtime是go語言執行所需要的基礎設施,也是go的核心特性。 該內容將放到後續《go基礎之特性》章節為大家分享。

使用

案例:init初始化順序

package main

import "fmt"

var Num int = Call() // 全域性變數宣告

func init() { // 初始化函式
    fmt.Println("init()")
}

func Call() int {
    fmt.Println("Call()")
    return 1
}

func main() {
    fmt.Println("main()")
}

輸出

Call()
init()
main()

結論,初始化的過程:Num變數初始化 -> init() -> main()

案例:同一個包不同原始碼的init初始化順序

首先建立3個檔案, main.go程式碼中包含全域性變數、init初始化函式定義,和main函式入口; a.go 和 b.go程式碼檔案中,只包含全域性變數、init初始化函式的定義。

main.go檔案

package main

import (
    "fmt"
)

var _ int = m()

func init() {
   fmt.Println("init in main.go")
}

func m() int {
   fmt.Println("call m() in main.go")
   return 1
}

func main() {
   fmt.Println("main()")
}

a.go 檔案

package main

import "fmt"

var _ int = a()

func init() {
   fmt.Println("init in a.go")
}

func a() int {
   fmt.Println("call a() in a.go")
   return 1
}

b.go 檔案

package main

import "fmt"

var _ int = b()

func init() {
   fmt.Println("init in b.go")
}

func b() int {
   fmt.Println("call b() in b.go")
   return 1
}
因為a.go 和 b.go 都歸屬於main包,但沒有兩檔案中沒有main函式入口。 在執行的時候,需要使用 go run main.go a.go b.go 這樣形式執行,runtime會將所有檔案進行載入初始化。

輸出

call m() in main.go
call a() in a.go
call b() in b.go
init in main.go
init in a.go
init in b.go
main()

結論,同一個包不同原始檔的init函式執行順序,golang 沒做官方說明。這塊載入過程是按照 go run 檔案排序。

案例:多個init函式初始化順序

package main

import "fmt"

func init() {
   fmt.Println("init 1")
}

func init() {
   fmt.Println("init 2")
}

func main() {
   fmt.Println("main")
}

輸出

init 1
init 2
main

結論:init函式比較特殊,可以在包裡被多次定義。

方法

介紹

Golang中方法,實現是以繫結物件例項, 並隱式將例項作為第一實參 (receiver)。

定義說明

  • 只能為當前包內命名型別定義方法;
  • 引數 receiver 可任意命名,如方法中未曾使用,可省略引數名;
  • 引數 receiver 型別可以是 T 或 *T, 基型別 T 不能是介面或指標;
  • 不支援方法過載, receiver 只是引數簽名的組成部分;
  • 可用例項 valuepointer 呼叫全部方法, 編譯器自動轉換

一個方法就是一個包含了接受者的函式, 接受者可以是命名型別或者結構體型別的一個值或者是一個指標。

方法定義

func (recevier type) methodName(引數列表) (返回值列表) {} // 引數和返回值可以省略

使用

定義一個結構型別和該型別的一個方法

package main

import "fmt"

// 結構體
type Info struct {
    Name  string
    Desc string
}

// 方法
func (u Info) Output() {
    fmt.Printf("%v: %v \n", u.Name, u.Desc)
}

func main() {
    // 值型別呼叫方法
    u1 := Info{"帽兒山的槍手", "分享技術文章"}
    u1.Output()
    // 指標型別呼叫方法
    u2 := Info{"帽兒山的槍手", "分享技術文章"}
    u3 := &u2
    u3.Output()
}

輸出

帽兒山的槍手: 分享技術文章 
帽兒山的槍手: 分享技術文章

匿名方法

如型別S包含匿名欄位 *T ,則 S 和 *S 方法集包含 T + *T 方法。

這條規則說的是當我們嵌入一個型別的指標, 嵌入型別的接受者為值型別或指標型別的方法將被提升, 可以被外部型別的值或者指標呼叫。
package main

import "fmt"

type S struct {
    T
}

type T struct {
    int
}

func (t T) testT() {
    fmt.Println("如型別 S 包含匿名型別 *T, 則 S 和 *S 方法集包含 T 方法")
}

func (t *T) testP() {
    fmt.Println("如型別 S 包含匿名欄位 *T, 則 S 和 *S 方法集合包含 *T 方法")
}

func main() {
    s1 := S{T{1}}
    s2 := &s1
    fmt.Printf("s1 is : %v\n", s1)
    s1.testT()
    s1.testP() // 提升指標型別呼叫

    fmt.Printf("s2 is : %v\n", s2)
    s2.testT() // 提升值型別呼叫
    s2.testP()
}

輸出

s1 is : {{1}}
如型別 S 包含匿名型別 *T, 則 S 和 *S 方法集包含 T 方法
如型別 S 包含匿名欄位 *T, 則 S 和 *S 方法集合包含 *T 方法
s2 is : &{{1}}
如型別 S 包含匿名型別 *T, 則 S 和 *S 方法集包含 T 方法
如型別 S 包含匿名欄位 *T, 則 S 和 *S 方法集合包含 *T 方法

表示式

根據呼叫者不同,方法分為兩種表現形式

instance.method(args...) ---> <type>.func(instance, args...)
前者稱為 method value, 後者 method expression則須顯式傳參。
package main

import "fmt"

type User struct {
    id   int
    name string
}

func (self *User) Test() {
    fmt.Printf("%p, %v\n", self, self)
}

func main() {
    u := User{1, "帽兒山的槍手"}
    u.Test()

    mValue := u.Test
    mValue() // 隱式傳遞 receiver

    mExpression := (*User).Test
    mExpression(&u) // 顯式傳遞 receiver
}

輸出

0xc00000c018, &{1 帽兒山的槍手}
0xc00000c018, &{1 帽兒山的槍手}
0xc00000c018, &{1 帽兒山的槍手}

結論,方法是指標型別,method value 會複製 receiver。

技術文章持續更新,請大家多多關注呀~~

搜尋微信公眾號【 帽兒山的槍手 】,關注我

相關文章