Go語(yǔ)言必知的5個(gè)核心知識(shí)點(diǎn)之init、路徑、輸出、切片、Map
init 函數(shù)
Go 中的 init() 函數(shù)是特殊的初始化函數(shù),用于包級(jí)別的初始化操作,它的執(zhí)行時(shí)機(jī)有嚴(yán)格的規(guī)則,是 Go 程序啟動(dòng)流程中固定的一環(huán)。在當(dāng)前包被導(dǎo)入 / 程序啟動(dòng)時(shí),init() 函數(shù)會(huì)在 main 函數(shù)執(zhí)行之前,自動(dòng)被 Go 運(yùn)行時(shí)調(diào)用執(zhí)行,無(wú)需手動(dòng)調(diào)用。
執(zhí)行順序(優(yōu)先級(jí)從高到低)
Go 程序的初始化執(zhí)行流程是固定的:
1、單個(gè)文件/包內(nèi)規(guī)則
- 一個(gè)源文件中可以寫(xiě)多個(gè) init() 函數(shù),按代碼順序從上到下的順序執(zhí)行
- 一個(gè)包下有多個(gè)文件,每個(gè)文件的
init()都會(huì)執(zhí)行,執(zhí)行順序由編譯器的文件編譯順序決定 init()函數(shù)無(wú)參數(shù)、無(wú)返回值,不能被手動(dòng)調(diào)用
2、多個(gè)文件
- 如果程序?qū)肓硕鄠€(gè)包,且不相互依賴(lài),按照import順序執(zhí)行
- 不同package且相互依賴(lài),最后被依賴(lài)的最先被執(zhí)行
示例:main 包導(dǎo)入 pkgA,pkgA 導(dǎo)入 pkgB,執(zhí)行順序從最底層的pkgB開(kāi)始執(zhí)行
Go語(yǔ)言執(zhí)行順序
一、全局順序
1、初始化被依賴(lài)的包(遞歸執(zhí)行,深度優(yōu)先)
2、初始化當(dāng)前包(包級(jí)別 常量,包級(jí)別 變量(按聲明順序),執(zhí)行包內(nèi)的 init() 函數(shù))
3、最后執(zhí)行主文件中的main()函數(shù)
二、單個(gè) Go 文件內(nèi)部執(zhí)行順序(最常用)
同一個(gè) .go 文件里,執(zhí)行順序嚴(yán)格如下:
1、import 導(dǎo)入(只觸發(fā)依賴(lài)包初始化)
2、包級(jí)常量初始化
3、包級(jí)變量初始化
4、文件內(nèi)的 init 函數(shù)(從上往下)
5、main 函數(shù)(如果是 main 包)
三、總結(jié)中的關(guān)鍵點(diǎn)
1、init 函數(shù)自動(dòng)執(zhí)行,不能手動(dòng)調(diào)用
2、一個(gè)文件可以寫(xiě)多個(gè) init,按從上到下執(zhí)行
3、一個(gè)包無(wú)論被導(dǎo)入多少次,init 只執(zhí)行一次
4、初始化順序:常量 → 變量 → init → main
5、多包依賴(lài):深度優(yōu)先,被依賴(lài)包先初始化
項(xiàng)目根目錄
在GO語(yǔ)言開(kāi)發(fā)中,在 go run、編譯二進(jìn)制、docker、不同工作目錄、不同系統(tǒng)下,是 5 個(gè)不同的路徑,所以讀取項(xiàng)目路徑顯得尤為重要:
os.Executable
是最穩(wěn)定、最推薦的方法,不管是 go run 還是運(yùn)行編譯后的二進(jìn)制文件,都能拿到正確路徑。
package main
import (
"fmt"
"os"
"path/filepath"
)
// GetProjectRoot 獲取項(xiàng)目根目錄
func GetProjectRoot() (string, error) {
// 獲取當(dāng)前可執(zhí)行文件的絕對(duì)路徑
exePath, err := os.Executable()
if err != nil {
return "", err
}
// 獲取可執(zhí)行文件所在目錄
exeDir := filepath.Dir(exePath)
// 如果是 go run 模式,exe 在臨時(shí)目錄,需要向上找 go.mod
if isGoRun() {
return findGoModDir(exeDir)
}
return exeDir, nil
}
// 判斷是否是 go run 運(yùn)行
func isGoRun() bool {
return filepath.Base(os.Args[0]) == "go" ||
filepath.Base(os.Args[0]) == "go.exe" ||
filepath.HasPrefix(os.Args[0], os.TempDir())
}
// 向上查找 go.mod 所在目錄 = 項(xiàng)目根目錄
func findGoModDir(startDir string) (string, error) {
dir := startDir
for {
modPath := filepath.Join(dir, "go.mod")
if _, err := os.Stat(modPath); err == nil {
return dir, nil
}
parent := filepath.Dir(dir)
// 已經(jīng)到根目錄還沒(méi)找到,退出
if parent == dir {
break
}
dir = parent
}
return "", fmt.Errorf("未找到 go.mod,不是 Go 項(xiàng)目")
}
func main() {
root, err := GetProjectRoot()
if err != nil {
panic(err)
}
fmt.Println("項(xiàng)目根目錄:", root)
}
slices包
Go 1.21 提供了 slices 包,也可以用更簡(jiǎn)潔的方式獲取
package main
import (
"log"
"os"
"path/filepath"
"golang.org/x/tools/go/packages"
)
func main() {
cfg := &packages.Config{Mode: packages.NeedModule}
pkgs, err := packages.Load(cfg, ".")
if err != nil {
log.Fatal(err)
}
root := pkgs[0].Module.Dir
log.Println("項(xiàng)目根目錄:", root)
}
環(huán)境變量
- 通過(guò)GOPATH或者自定義項(xiàng)目根路徑的環(huán)境變量
- 利用系統(tǒng)自帶的環(huán)境變量和路徑
輸出打印
Go 語(yǔ)言通過(guò)標(biāo)準(zhǔn)庫(kù) fmt 包 實(shí)現(xiàn)格式化輸出,核心函數(shù)是 fmt.Print()、fmt.Println()、fmt.Printf(),其中 fmt.Printf() 支持最靈活的格式化輸出,是開(kāi)發(fā)中最常用的。
| 函數(shù) | 作用 | 特點(diǎn) |
|---|---|---|
fmt.Print(a...) | 普通輸出 | 不換行,多個(gè)參數(shù)直接拼接 |
fmt.Println(a...) | 換行輸出 | 自動(dòng)換行,參數(shù)間加空格 |
fmt.Printf(format, a...) | 格式化輸出 | 按指定占位符格式輸出,不自動(dòng)換行 |
占位符以 % 開(kāi)頭,后跟類(lèi)型標(biāo)識(shí),是 fmt.Printf 的核心。
| 占位符 | 作用 |
|---|---|
%v | 默認(rèn)格式輸出(最常用) |
%+v | 輸出結(jié)構(gòu)體時(shí),顯示字段名 |
%#v | 輸出 Go 語(yǔ)法格式的值(調(diào)試用) |
%T | 輸出值的類(lèi)型 |
代碼示例:
type User struct {Name string}
u := User{Name: "stark張宇"}
fmt.Printf("%v\n", u) // {stark張宇}
fmt.Printf("%+v\n", u) // {Name:stark張宇}
fmt.Printf("%T\n", u) // main.User
fmt.Printf("%%\n") // %
打印 布爾 / 字符串 / 指針占位符/十進(jìn)制整數(shù)
| 占位符 | 作用 |
|---|---|
%t | 輸出布爾值:true/false |
%s | 輸出字符串 / 字節(jié)切片 |
%q | 輸出帶雙引號(hào)的字符串 |
%p | 輸出指針地址(十六進(jìn)制) |
%d | 10 進(jìn)制整數(shù) |
%#v | 打印 map |
str := "hello"
fmt.Printf("%s\n", str) // hello
fmt.Printf("%q\n", str) // "hello"
fmt.Printf("%p\n", &str) // 0x14000010200
fmt.Printf("map = %#v\n", m) //
fmt.Printf("執(zhí)行到:%s:%d\n", __FILE__, __LINE__)
fmt.Printf("a=%v, b=%v, c=%v\n", a, b, c)
數(shù)組Array、切片Slice
它們的共同點(diǎn)是都屬于集合類(lèi)的類(lèi)型,并且,它們的值也都可以用來(lái)存儲(chǔ)某一種類(lèi)型的值(或者說(shuō)元素)。它們最重要的不同是:數(shù)組類(lèi)型的值的長(zhǎng)度是固定的,而切片類(lèi)型的值是可變長(zhǎng)的。
切片的類(lèi)型字面量中只有元素的類(lèi)型,而沒(méi)有長(zhǎng)度,切片的長(zhǎng)度可以自動(dòng)地隨著其中元素?cái)?shù)量的增長(zhǎng)而增長(zhǎng),但不會(huì)隨著元素?cái)?shù)量的減少而減小。
數(shù)組和切片的關(guān)系: 可以把切片看做是對(duì)數(shù)組的一層簡(jiǎn)單的封裝,因?yàn)樵诿總€(gè)切片的底層數(shù)據(jù)結(jié)構(gòu)中,一定會(huì)包含一個(gè)數(shù)組。數(shù)組可以被叫做切片的底層數(shù)組,而切片也可以被看作是對(duì)數(shù)組的某個(gè)連續(xù)片段的引用。
切片的長(zhǎng)度和容量
s1 := make([]int, 5)
fmt.Printf("S1 length : %d , capacity %d ,value %d \n ", len(s1), cap(s1), s1)
s2 := make([]int, 5, 8)
fmt.Printf("S2 length : %d , capacity %d ,value %d \n ", len(s2), cap(s2), s2)
用make函數(shù)初始化切片時(shí),如果不指明其容量,那么它就會(huì)和長(zhǎng)度一致。如果在初始化時(shí)指明了容量,那么切片的實(shí)際容量也就是它了。這也正是s2的容量是8的原因。
通過(guò)調(diào)用內(nèi)建函數(shù)len,得到數(shù)組和切片的長(zhǎng)度,通過(guò)調(diào)用內(nèi)建函數(shù)cap,我們可以得到它們的容量,但要注意,數(shù)組的容量永遠(yuǎn)等于其長(zhǎng)度,都是不可變的。
Go語(yǔ)言 1.18+ 后 ,當(dāng)執(zhí)行 append 時(shí),新長(zhǎng)度 len(s)+n > 當(dāng)前容量 cap(s) ,采用 分段式擴(kuò)容 + 內(nèi)存對(duì)齊,核心是 小切片翻倍、大切片 1.25 倍漸進(jìn)增長(zhǎng)。
規(guī)則:小切片(<256):2 倍擴(kuò)容,大切片(≥256):~1.25 倍漸進(jìn)增長(zhǎng),這里的256有一個(gè)常見(jiàn)的誤區(qū),256 就是純數(shù)字 256,沒(méi)有任何單位(不是 KB、不是 MB),是元素個(gè)數(shù),cap的切片元素容量。
// 1. 容量 cap = 100 < 256 → 小切片 s1 := make([]int, 0, 100) // 2. 容量 cap = 300 > 256 → 大切片 s2 := make([]int, 0, 300)
一個(gè)切片的底層數(shù)組永遠(yuǎn)不會(huì)被替換,當(dāng)切片在擴(kuò)容的時(shí)候 Go 語(yǔ)言一定會(huì)生成新的底層數(shù)組,但是它也同時(shí)生成了新的切片。需要注意的是在無(wú)需擴(kuò)容時(shí),append函數(shù)返回的是指向原底層數(shù)組的新切片,而在需要擴(kuò)容時(shí),append函數(shù)返回的是指向新底層數(shù)組的新切片。
只要新長(zhǎng)度不會(huì)超過(guò)切片的原容量,那么使用append函數(shù)對(duì)其追加元素的時(shí)候就不會(huì)引起擴(kuò)容,這只會(huì)使緊鄰切片窗口右邊的(底層數(shù)組中的)元素被新的元素替換掉。
擴(kuò)容時(shí)底層內(nèi)存變化
當(dāng)切片發(fā)生擴(kuò)容時(shí),判斷容量不夠,Go 會(huì)申請(qǐng)一塊新的、更大的連續(xù)內(nèi)存空間生成新底層數(shù)組,把舊數(shù)組里的所有元素復(fù)制到新數(shù)組,把新追加的元素也放進(jìn)去,切片內(nèi)部的指針從舊數(shù)組地址切換到新數(shù)組,舊數(shù)組只要還有引用,舊數(shù)組就會(huì)一直占內(nèi)存,如果沒(méi)有任何變量、切片再引用舊數(shù)組,Go 垃圾回收(GC)會(huì)在未來(lái)某個(gè)時(shí)間自動(dòng)回收
字典 Map
Map這個(gè)詞的本意在數(shù)學(xué)里指的是鍵值對(duì)的映射集合體,Go 語(yǔ)言的字典類(lèi)型其實(shí)是一個(gè)哈希表(hash table)的特定實(shí)現(xiàn),哈希表會(huì)先用哈希函數(shù)(hash function)把鍵值轉(zhuǎn)換為哈希值。哈希值通常是一個(gè)無(wú)符號(hào)的整數(shù),一個(gè)哈希表會(huì)持有一定數(shù)量的桶(bucket),元素就存在哈希桶中。
每個(gè)哈希桶都會(huì)把自己包含的所有鍵的哈希值存起來(lái),Go 語(yǔ)言會(huì)用被查找鍵的哈希值與這些哈希值逐個(gè)對(duì)比,看看是否有相等的,如果一個(gè)相等的都沒(méi)有,那么就說(shuō)明這個(gè)桶中沒(méi)有要查找的鍵值,這時(shí) Go 語(yǔ)言就會(huì)立刻返回結(jié)果了。
因?yàn)樯鲜鏊f(shuō)字典中鍵的查找原理,就要求鍵類(lèi)型的值必須要支持判等操作,要求鍵類(lèi)型不可以是函數(shù)類(lèi)型、字典類(lèi)型和切片類(lèi)型,而元素卻可以是任意類(lèi)型的, 否則在程序運(yùn)行過(guò)程中會(huì)引發(fā) panic。
var userAge map[string]int{
"張三": 20,
"李四": 25,
"王五": 22,
}
for name, age := range userAge {
fmt.Printf("姓名:%s,年齡:%d\n", name, age)
}
因?yàn)槊總€(gè)map的鍵都要進(jìn)行哈希值的計(jì)算和查找,所以寬度越小的類(lèi)型速度通常越快,類(lèi)型的寬度是指它的單個(gè)值需要占用的字節(jié)數(shù)。比如,bool、int8和uint8類(lèi)型的一個(gè)值需要占用的字節(jié)數(shù)都是1,因此這些類(lèi)型的寬度就都是1。
總結(jié)
到此這篇關(guān)于Go語(yǔ)言必知的5個(gè)核心知識(shí)點(diǎn)之init、路徑、輸出、切片、Map的文章就介紹到這了,更多相關(guān)Go語(yǔ)言init、路徑、輸出、切片、Map內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
聊聊Go語(yǔ)言編譯github上的項(xiàng)目遇到的坑
這篇文章主要介紹了解決Go語(yǔ)言編譯github上的項(xiàng)目遇到的坑,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-04-04
Golang程序中使用Prometheus的client_golang庫(kù)
這篇文章主要介紹了Golang程序中使用Prometheus的client_golang庫(kù),Prometheus 是一個(gè)開(kāi)源的監(jiān)控和警報(bào)工具包,用于收集和處理應(yīng)用程序和系統(tǒng)的指標(biāo)數(shù)據(jù)。Prometheus 提供了多種客戶(hù)端庫(kù),可以輕松地集成到各種編程語(yǔ)言中2023-04-04

