Golang 基礎:Go Module, for range, slice, map, struct 等使用和實現

語言: CN / TW / HK

專案裡使用 Go 開發後端,花了些時間系統的學習,這裡做個總結。

本文內容整理自極客時間 《Go 語言第一課》的學習筆記及日常總結。

Go 程式結構

https://time.geekbang.org/column/article/428267

Go 的命名規則: - Go 原始檔總是用全小寫字母形式的短小單詞命名,並且以.go 副檔名結尾 - 如果要在原始檔的名字中使用多個單詞,我們通常直接是將多個單詞連線起來作為原始檔名,而不是使用其他分隔符,比如下劃線。也就是說,我們通常使用 helloworld.go 作為檔名而不是 hello_world.go。

import "fmt": - "fmt”代表的是包的匯入路徑(Import),它表示的是標準庫下的 fmt 目錄,整個 import 宣告語句的含義是匯入標準庫 fmt 目錄下的包 - 通常匯入路徑的最後一個分段名與 使用的包名是相同的

package main: - 包是 Go 語言的基本組成單元,一個 Go 程式本質上就是一組包的集合 - main 包在 Go 中是一個特殊的包,整個 Go 程式中僅允許存在一個名為 main 的包

func main: - 執行一個可執行的 go 程式時,入口就是 main 函式 - 只有首字母為大寫的函式才是匯出的,才能被人呼叫;如果首字母是小寫,則說明只在宣告的包內可見

函式內部: - 標準 Go 程式碼風格使用 Tab 而不是空格來實現縮排的

go build main.go - Go 是一種編譯型語言,這意味著只有你編譯完 Go 程式之後,才可以將生成的可執行檔案交付於其他人 - go生成的應用程式不依賴環境就可以執行(對方不需要安裝 go 就可以執行) - 開發階段可以使用 go run main.go 直接執行

如果你交付給其他人的是一份.rb、.py 或.js 的動態語言的原始檔,那麼他們的目標環境中就必須要擁有對應的 Ruby、Python 或 JavaScript 實現才能解釋執行這些原始檔

Go module 構建模式: - Go 1.11 版本正式引入的,為的是徹底解決 Go 專案複雜版本依賴的問題 - Go 預設的包依賴管理機制和 Go 原始碼構建機制 - 一個 module 就是一個包的集合,這些包和 module 一起打版本、釋出和分發。go.mod 所在的目錄被我們稱為它宣告的 module 的根目錄 - go.mod 檔案,儲存這個 module 對第三方的依賴資訊(一個 go.mod 檔案代表一個包,一個專案可以有多個 go.mod) - go mod init github.com/shixinzhang/hellomodule1: 生成一個 go.mod 檔案 - go mod tidy 可以根據 .go 檔案裡的依賴,自動下載和新增依賴 - go.sum 檔案:記錄直接/間接依賴庫的 hash 值,在構建時會檢查本地庫版本和這個檔案裡的雜湊值是否一致 - Go Module 本身就支援可再現構建,而無需使用 vendor。 當然 Go Module 機制也保留了 vendor 目錄(通過 go mod vendor 可以生成 vendor 下的依賴包,通過 go build -mod=vendor 可以實現基於 vendor 的構建)

``` admin@C02ZL010LVCK hellomodule % go mod tidy go: finding module for package go.uber.org/zap go: finding module for package github.com/valyala/fasthttp go: downloading github.com/valyala/fasthttp v1.34.0 go: found github.com/valyala/fasthttp in github.com/valyala/fasthttp v1.34.0 go: found go.uber.org/zap in go.uber.org/zap v1.21.0 go: downloading github.com/andybalholm/brotli v1.0.4 go: downloading github.com/klauspost/compress v1.15.0 admin@C02ZL010LVCK hellomodule % ls
go.mod go.sum main.go admin@C02ZL010LVCK hellomodule % cat go.mod module github.com/shixinzhang/hellomodule1

go 1.16

require ( github.com/valyala/fasthttp v1.34.0 go.uber.org/zap v1.21.0 ) admin@C02ZL010LVCK hellomodule % ```

專案結構

https://time.geekbang.org/column/article/429143

兩種專案: 1. 可執行程式 2. 庫專案

可執行程式

  • go.mod go.sum 放在專案根目錄
  • cmd 目錄:存放要構建的可執行檔案對應的 main 包原始碼
  • 其他程式碼按照不同包,放在對應的目錄下
  • internal 目錄:存放內部使用,外部無法訪問的 Go 包

通常來說,main 包應該很簡潔。我們在 main 包中會做:命令列引數解析、資源初始化、日誌設施初始化、資料庫連線初始化等工作

之後就會將程式的執行許可權交給更高階的執行控制物件

Reproducible Build: 可重現構建,就是針對同一份go module的原始碼進行構建,不同人,在不同機器(同一架構,比如都是x86-64),相同os上,在不同時間點都能得到相同的二進位制檔案。

庫專案

可執行程式的簡化版,去掉 cmd 和 vendor 目錄就是了。

Go 庫專案的初衷是為了對外部(開源或組織內部公開)暴露 API,對於僅限專案內部使用而不想暴露到外部的包,可以放在專案頂層的 internal 目錄下面。

Go 專案結構沒有絕對的標準:https://github.com/golang-standards/project-layout/issues/117#issuecomment-828503689

Go Module 構建模式

https://time.geekbang.org/column/article/429941

Go 程式構建過程: 1. 確定包版本 2. 編譯包 3. 將編譯後的目標檔案連結到一起

Go 語言的構建模式歷經了三個迭代和演化過程: 1. GOPATH:去本地環境變數目錄下查詢依賴的庫 2. Vendor:把依賴庫的程式碼下載到 vendor 下,一起提交。查詢依賴時,先從 vendor 目錄查詢 3. Go Module: go.mod 及背後的機制

GOPATH: 可以通過 go get 命令將本地缺失的第三方依賴包(還有它的依賴)下載到本地 GOPATH 環境變數配置的路徑。

先找 $GOROOT 然後找 $GOPATH

在沒有 go module 機制前,go get 下載的是當時最新的。如果別人在不同時間去執行,可能和你下載的庫版本不一致。 go env GOPATH 檢視本地環境變數

vendor:

要想開啟 vendor 機制,你的 Go 專案必須位於 GOPATH 環境變數配置的某個路徑的 src 目錄下面。如果不滿足這一路徑要求,那麼 Go 編譯器是不會理會 Go 專案目錄下的 vendor 目錄的。

Go Module:

go.mod 檔案將當前專案變為了一個 Go Module,專案根目錄變成了 module 根目錄

  1. go mod init: 建立 go.mod 檔案,將一個 Go 專案轉變為一個 Go Module
  2. go mod tidy:掃描 Go 原始碼,並自動找出專案依賴的外部 Go Module 以及版本,下載這些依賴並更新依賴資訊到 go.mod 檔案中,生成校驗和檔案
  3. go build 執行構建:讀取 go.mod 中的依賴及版本資訊,並在本地 module 快取路徑下找到對應版本的依賴 module,執行編譯和連結

相關環境變數: - GOPROXY:下載的代理服務 - GOMODCACHE:下載到哪裡 admin@C02ZL010LVCK ~ % go env GOPROXY https://proxy.golang.org,direct admin@C02ZL010LVCK ~ % go env GOMODCACHE /Users/simon/go/pkg/mod

語義匯入版本

  • v1.2.1,主版本號(major)、次版本號(minor)、補丁版本號(patch)
  • 預設主版本號不同時,不相容;次版本號和補丁版本號提升後,向前相容
  • 如果主版本號升級,需要在匯入路徑裡增加版本號:import "github.com/sirupsen/logrus/v2",這樣 Go Module 機制就會去 v2 路徑下查詢庫

Go 的“語義匯入版本”機制:通過在包匯入路徑中引入主版本號的方式,來區別同一個包的不相容版本,這樣一來我們甚至可以同時依賴一個包的兩個不相容版本。

最小版本選擇原則

A 和 B 有一個共同的依賴包 C,但 A 依賴 C 的 v1.1.0 版本,而 B 依賴的是 C 的 v1.3.0 版本,並且此時 C 包的最新發布版為 C v1.7.0 Go 命令會選擇 v1.3.0,相容 2 個庫的最小版本,而不會擅自選擇最新版本

最小版本選擇更容易實現可重現構建。

試想一下,如果選擇的是最大最新版本,那麼針對同一份程式碼,其依賴包的最新最大版本在不同時刻可能是不同的,那麼在不同時刻的構建,產生的最終檔案就是不同的。

可以通過 GO111MODULE 環境變數進行構建模式的切換。

但要注意,從 Go 1.11 到 Go 1.16,不同版本在 GO111MODULE 配置不同時,使用的構建模式不一樣。

Go Module 的常規操作

https://time.geekbang.org/column/article/431463

空匯入: - import _ "foo"
- 空匯入只是引入這個包,常見於引入mysql驅動,但是卻不使用這個包中暴露的方法,有些包是依賴驅動實現的 - 空匯入意味著期望依賴包的init函式得到執行,這個init函式中有我們需要的邏輯。

go 私有倉庫:

私有代理做的比較好的有goproxy.io、goproxy.cn、athen等

1.新增依賴

  1. 程式碼里加上 import 語句
  2. 執行 go get,會下載並更新 go.mod
  3. go mod tidy 也能達到類似的效果,但比 go get 更好,尤其在複雜專案裡

對於複雜的專案變更而言,逐一手工新增依賴項顯然很沒有效率,go mod tidy 是更佳的選擇

imported and not used: "github.com/google/uuid"

go mod tidy 有點類似 pip install -r requirements.txt

2.升級/降級依賴

go list -m -versions github.com/gin-gonic/gin 檢視某個庫的所有版本號

升級、降級,也是使用 go getgo mod tidy,區別在於引數

go get 庫名@版本號:go get github.com/gin-gonic/[email protected] 會下載指定的版本,同時更新 go.mod 裡的配置版本號

go mod: - 先用 go mod edit 修改版本號: go mod edit -require=庫名@版本號:go mod edit -require=github.com/gin-gonic/[email protected] - 然後執行 go mod tidy

3.新增一個主版本號大於 1 的依賴

之所以主版本號大於 1 特殊,是因為一般來說主版本號不同,是大升級,不向前相容。

如果新版本號和之前的不相容,就不能使用預設的庫名方式匯入,而需要在庫名後,加上版本號:

import github.com/user/repo/v2/xxx

然後再執行 go get 什麼的,就和之前的一樣了。

4.刪除依賴

刪除這個庫的匯入語句後,執行 go mod tidy 就可以了,真不愧它的名稱,處理的乾乾淨淨。

go mod tidy 會自動分析原始碼依賴,而且將不再使用的依賴從 go.mod 和 go.sum 中移除

5.vendor 相關

執行 go mod vendor 會建立一個 vendor 目錄,然後把依賴的庫的程式碼都複製一份到這裡。其中的 modules.txt 檔案也會記錄下庫的版本號。

如果要基於 vendor 構建,而不是基於本地快取的 Go Module 構建,需要在 go build 後面加上 -mod=vendor 引數。

高版本(1.14 以後),如果有 vendor 目錄,go build 會優先從 vendor 查詢依賴。

入口函式與包初始化:搞清Go程式的執行次序

https://time.geekbang.org/column/article/432021

Go 應用的入口函式:main 包中的 main 函式

如果要在 main.main 函式之前執行一些工作,可以定義一個 init 函式,在其中進行。

每個 Go 包可以擁有不止一個 init 函式,每個組成 Go 包的 Go 原始檔中,也可以定義多個 init 函式

##【執行流程圖】

Go 包是程式邏輯封裝的基本單元,每個包都可以理解為是一個“自治”的、封裝良好的、對外部暴露有限介面的基本單元 程式的初始化就是這些包的初始化。

初始化順序:

  1. 按照匯入順序,遞迴初始化依賴的所有包(以及他們依賴的包)的內容
  2. 某個包的初始化順序:常量 -> 變數 -> init 函式
  3. 如果是 main 包的話,然後執行 main 函式

多個包依賴的包僅會初始化一次

init 函式的用途

對包級變數的初始化狀態進行檢查和修改,比如有些必須設定的引數,呼叫方沒設定或者設定的有問題,可以在這裡兜底。

每個 init 函式在整個 Go 程式生命週期內僅會被執行一次。

還可以根據配置(比如環境變數),修改變數的值,比如 url 等,挺實用的。

還有一個非常常見的應用場景:結合空匯入,實現一些解耦性很強的設計。

比如訪問資料庫,一般會空匯入一個具體的驅動實現(mysql 或者 postgres),在這個呼叫的檔案初始化時,會執行到驅動實現的檔案初始化,從而執行它的 init 方法,向 sql 庫中注入一個具體的驅動實現。

``` import ( "database/sql" _ "github.com/go-sql-driver/mysql" //空匯入 )

db, err = sql.Open("mysql", "root:password@tcp(127.0.0.1:3306)/todo")

//mysql 的 driver.go func init() { sql.Register("mysql", &MySQLDriver{}) } ```

從標準庫 database/sql 包的角度來看,這種“註冊模式”實質是一種工廠設計模式的實現,sql.Open 函式就是這個模式中的工廠方法,它根據外部傳入的驅動名稱“生產”出不同類別的資料庫例項控制代碼

使用內建包實現一個簡單的 Web 服務 【待學完回來敲】

https://time.geekbang.org/column/article/434017

變數宣告

https://time.geekbang.org/column/article/435858

## 【第一張圖和小結裡的圖貼一下】 在這裡插入圖片描述

靜態語言宣告的意義在於告訴編譯器該變數可以操作的記憶體的邊界資訊(幾個位元組)。

不同型別的零值:

  • 整數型別:0
  • 浮點型別:0.0
  • 字串型別:""
  • 指標、介面、切片、channel、map、函式:nil

``` package test

import "fmt"

//包級變數 var Age int var ( name string = "shixinzhang" address = "Shanghai" //省略型別 a, b, c = 1, 2.1, 'c' //一行宣告多個,省略型別 )

func TestVariable() { var height int = 128 var h = int32(128) //顯式型別轉換 等同於下面這個 var h32 int32 = 128

var a, b, c int = 1,2,3 //一行宣告多個變數,型別其實可以推匯出來,逗號不能少!

weight := 140   //短變數宣告,省略 var 和型別
d, e, f := 4,5, "hi"    //短變數也可以宣告多個,不同型別也可以

fmt.Println("height ", height, h, h32, weight, a,b,c, d,e,f)

} ```

宣告方式: - 通用變數宣告 - 短變數宣告

通用變數宣告:

var a int = 10

變數名在型別的前面,和 typescript 一樣。

之所以這樣,原因簡單來:和C相比,在當引數是指標的複雜情況下,這種宣告格式會相對好理解一點。

宣告的時候也可以不賦值,會有預設值,稱為零值

變數宣告塊:用一個 var 關鍵字,包括多個變數宣告:

var ( name string = "shixinzhang" address = "Shanghai" //省略型別 a, b, c = 1, 2.1, 'c' //一行宣告多個,省略型別 )

短變數宣告

短變數宣告(:=):省去 var 關鍵字和型別資訊

a := 12 a, b, c := 12, 'B', "CC"

變數型別會由編譯器自動推匯出來。

Go 中的變數型別: - 包級變數、匯出(首字母大寫的包級變數)變數 - 區域性變數

包級變數的宣告形式

1.宣告的同時直接初始化

var ErrShortWrite = errors.New("short write")

2.先宣告,稍後初始化

宣告聚類:把延遲初始化和直接顯式初始化的變數放到不同的宣告塊中。 可以提升程式碼可讀性。

就近原則: 變數儘可能地宣告在使用處附近

區域性變數的宣告形式

1.宣告時直接初始化

使用短變數宣告。

短變數宣告是區域性變數使用最多的宣告方式。

age := 28 name := "shixinzhang"

複雜型別不支援獲取預設型別,需要在等號右側增加顯式轉型:

s := []byte("hello shixinzhang")

2.先宣告,稍後初始化

因為沒有初始化值,所以需要宣告型別。使用通用變數宣告: 先 var a number,然後賦值。

如果有多個區域性變數需要宣告,也可以考慮使用 var 宣告塊完成。

func test() { var { age int name string } }

程式碼塊與作用域

https://time.geekbang.org/column/article/436915

作用域: - 變數僅在某一範圍內有效。 - 在這個範圍內,如果宣告和更大層級同名的變數,會重新建立一個,而不是使用全域性的變數。如果進行賦值,修改的也是當前範圍的 (變數遮蔽) - 退出這個程式碼塊後,變數不再可訪問。

顯式程式碼塊:使用 {} 包圍起來的程式碼。

隱式程式碼塊:

  1. 全域性/宇宙級程式碼塊
  2. 包級程式碼塊
  3. 檔案級程式碼塊
  4. 函式級程式碼塊
  5. 控制邏輯級程式碼塊

作用域最大的 Go 語言預定義識別符號:

在這裡插入圖片描述

同一個 package 中的不同檔案,不能有同名的包級變數! 假如在同一個包中的檔案 A 定義了全域性變數 a,那在這個包裡的其他檔案,都不能再定義全域性變數 a。

匯入其他包時,僅可使用其他包的匯出識別符號,匯出識別符號具有包程式碼塊級作用域。

匯出識別符號: 1. 宣告在包程式碼塊中(包中的全域性變數或方法) 2. 首字母大寫

匯入的包名的作用域是檔案程式碼塊

控制邏輯級程式碼的作用域:

  1. if 條件裡建立的,在 else 裡也可以訪問
  2. switch 條件裡建立的,在 case 結束後無法訪問

``` func bar() { if a := 1; false { } else if b := 2; false { } else if c := 3; false { //在 if 條件裡建立的臨時變數,在 else 裡也可以訪問 } else { println(a, b, c) }

//在 if 條件裡建立的臨時變數,在 else 裡也可以訪問
//因為這個建立等價於這樣:
{
    c := 3 // 變數c作用域始於此
    if false {

    } else {
        println(a, b, c)
    }
    // 變數c的作用域終止於此
}

} ```

注意⚠️:短變數宣告與控制語句的結合十分容易導致變數遮蔽問題,並且很不容易識別!

變數遮蔽如何解決:

  1. 可以藉助 go vet 進行變數遮蔽檢查
  2. 約定命名規則,避免重複

go vet 下載及使用: 1. 下載 go vet:go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest 需要梯子 2. 執行:go vet -vettool=$(which shadow) -strict test/variable.go

-strict 指定要檢測的檔案,執行結果:

```

command-line-arguments

test/variable.go:18:6: declaration of "a" shadows declaration at line 10 ```

約定好包級別的變數用長的名字,越是區域性的變數用越短小的名字,應該能夠解決一大部分變數遮蔽的問題。

基本資料型別:數值型別

https://time.geekbang.org/column/article/439782

Go 中的型別: 1. 基本資料型別 2. 複合資料型別 3. 介面型別

基本資料型別:整型、浮點型、複數型別

整型

整型分平臺無關平臺相關兩種(和 C/C++ 類似)。

區別在於:在不同的 CPU 架構和作業系統下,長度是否一致

在這裡插入圖片描述

平臺無關整形分為兩種,區別在於二進位制首位是表示數還是符號位: 1. 有符號 2. 無符號

符號位就是表示正負符號

Go 的整型位元位編碼:2 的補碼方式(按位取反再 + 1) 所以有符號的二進位制數 10000001 的整數值就是負的 01111110 + 1 = -127

在這裡插入圖片描述

平臺相關整形貌似就 3 個: 1. int: 32 位 4 位元組,64 位 8 位元組 2. uint:32 位 4,64 位 8 3. uintptr 在這裡插入圖片描述

在編寫可移植性程式時,不要使用平臺相關!

unsafe.Sizeof(a) 可以檢視變數的長度。

整型溢位

在使用名稱帶數字的整型時,要注意它的長度範圍,避免溢位。

``` func TestVariable() { ss := int32(12) int8_a := int8(127) int8_a += 1 //溢位!

uint8_b := uint8(1)
uint8_b -= 2 //溢位!
fmt.Println("sizeof int:", unsafe.Sizeof(a))
fmt.Println("sizeof int8:", unsafe.Sizeof(int8_a), int8_a)
fmt.Println("sizeof uint8:", unsafe.Sizeof(uint8_b), uint8_b)
fmt.Println("sizeof int32:", unsafe.Sizeof(ss))

} ```

比如上面使用了 int8 和 uint8,從名字我們知道它只有 8 個位元組,1 byte,所以表示範圍為 [-128, 127],在使用時如果不小心超出這個範圍,就會得到預期外的結果:

sizeof int: 8 sizeof int8: 1 -128 sizeof uint8: 1 255 sizeof int32: 4

以不同進位制格式化列印:

Printf: ``` func TestDiffFormatPrint() { value := 127 value8 := 010 value16 := 0x10

fmt.Printf("value8 十進位制:%d \n", value8)
fmt.Printf("value16 十進位制:%d \n", value16)
fmt.Printf("十進位制:%d \n", value)
fmt.Printf("二進位制:%b \n ", value)
fmt.Printf("八進位制:%o  \n", value)
fmt.Printf("八進位制帶字首:%O  \n", value)
fmt.Printf("十六進位制:%x \n ", value)
fmt.Printf("十六進位制帶字首:%X  \n", value)

} ```

輸出:

value8 十進位制:8 value16 十進位制:16 十進位制:127 二進位制:1111111 八進位制:177 八進位制帶字首:0o177 十六進位制:7f 十六進位制帶字首:7F

浮點數

和整型相比,浮點型別在二進位制表示和使用方面都更復雜!

Go 語言提供了 float32 與 float64 兩種浮點型別,它們分別對應的就是 IEEE 754 中的單精度與雙精度浮點數值型別。

Go 浮點型別與平臺無關。

在這裡插入圖片描述

浮點數的二進位制表示: - 符號位 - 階碼 - 尾數

階碼和尾數的長度決定了浮點型別可以表示的浮點數範圍與精度。

在這裡插入圖片描述 在這裡插入圖片描述

【浮點數十進位制轉二進位制的規則待仔細整理】

日常使用中儘量使用 float64,這樣不容易出現浮點溢位的問題

``` func TestFloatNumber() { value := float64(1.2)

fmt.Println("value: %d", value)

var fl1 float32 = 16777216.0
var fl2 float32 = 16777217.0
fmt.Println("16777216.0 == 16777217.0? ", fl1 == fl2);

bits := math.Float32bits(fl1)
bits_fl2 := math.Float32bits(fl2)
fmt.Printf("fl1 bits:%b \n", bits)
fmt.Printf("fl2 bits:%b \n", bits_fl2)

value3 := 6456.43e-2    //e-2 = 10^-2
value4 := .12345e4  //10^4
fmt.Printf("6456.43e-2 %f, .12345e4:%0.2f \n", value3, value4)

//輸出為科學計數法的形式
fmt.Printf("%e \n", 6543.21)  //十進位制的科學計數法
fmt.Printf("%x \n", 6543.21)    //十六進位制的科學計數法        //p/P 代表的冪運算的底數為 2

}

```

輸出:

value: %d 1.2 16777216.0 == 16777217.0? true fl1 bits:1001011100000000000000000000000 fl2 bits:1001011100000000000000000000000 6456.43e-2 64.564300, .12345e4:1234.50 6.543210e+03 0x1.98f35c28f5c29p+12

float32 型別的 16777216.0 與 16777217.0 相等,是因為他們的二進位制數一樣。因為 float32 的尾數只有 23bit。

複數型別

複數:z=a+bi,a 為實部,b 為虛部。

複數主要用於向量計算等場景。

Go 中實部和虛部都是浮點型別。

Go 複數有兩種型別:complex128 和 complex64,complex128 是預設型別。

``` func TestComplex() { //宣告一個複數 c := 5 + 6i

fmt.Println(reflect.TypeOf(c))

//real 獲取複數實部
//imag 獲取複數虛部
fmt.Printf("實部: %f, 虛部: %f \n", real(c), imag(c))

var _complex = complex(7.7, 8.8)
fmt.Printf("實部: %f, 虛部: %f \n", real(_complex), imag(_complex))

} ```

執行結果:

complex128 實部: 5.000000, 虛部: 6.000000 實部: 7.700000, 虛部: 8.800000

類型別名

Go 也支援類似 C/C++ 那樣的 typedef,有兩種方式:

type MyInt int32 //型別定義 type MyInt = int32 //類型別名

  • 第一種不加等號的,是等於新建立一個型別,這種型別和 int32 不能直接賦值,需要做強轉。
  • 第二種加等號的,就是一個別名,型別和 int32 一致,可以賦值。

``` type MyInt int32 type MyIntAlias = int32

func TestTypeDef() { age := int32(29)

height := MyInt(199)
weight := MyIntAlias(150)

//cannot use height (type MyInt) as type int32 in assignment
age = height    //編譯器報錯:Cannot use 'height' (type MyInt) as type int32
age = weight    //不報錯

} ```

基本資料型別:字串型別

https://time.geekbang.org/column/article/440804

  • 對比
  • 原理
  • 實操
  • 背後的設計以及常用方法

C 語言沒有提供字串型別的原生支援,是以’\0’結尾的字元陣列方式實現的。存在的問題:

  1. 字串操作時要時刻考慮結尾的’\0’,防止緩衝區溢位;
  2. 以字元陣列形式定義的“字串”,它的值是可變的,在併發場景中需要考慮同步問題
  3. 獲取一個字串的長度代價較大,strlen 通常是 O(n) 時間複雜度
  4. C 語言沒有內建對非 ASCII 字元(如中文字元)的支援

Go 中 string 的特性:

  1. 字元不可變:
  2. 只可以整體修改,不能單獨修改其中某個字元。保證了多執行緒訪問的安全性。
  3. 一個 value,在記憶體中只有一份
  4. 結尾不需要 '\0',獲取長度效率也很高
  5. 支援 raw string,由一對 `` 包圍即可
  6. 預設使用 Unicode 字符集,支援中文

舉個例子: 1. 按位元組輸出一個字串值 2. 使用 Go 在標準庫中提供的 UTF-8 包,對 Unicode 字元(rune)進行編解碼

``` func TestString() {

location := "中國人"

//1.按位元組輸出
fmt.Printf("the length of location is:%d\n", len(location)) //len: 位元組大小

for i:= 0; i < len(location); i++ {
    fmt.Printf("0x%x,", location[i])
}
fmt.Print("\n")

//2.按字元輸出
fmt.Println("the length of rune/character:", utf8.RuneCountInString(location))

for _, c := range location {
    fmt.Printf("%c | 0x%x , ", c, c)
}
fmt.Print("\n")

} ```

執行結果:

the length of location is:9 0xe4,0xb8,0xad,0xe5,0x9b,0xbd,0xe4,0xba,0xba, the length of rune/character: 3 中 | 0x4e2d , 國 | 0x56fd , 人 | 0x4eba ,

len 函式的作用:

// The len built-in function returns the length of v, according to its type: // Array: the number of elements in v. // Pointer to array: the number of elements in *v (even if v is nil). // Slice, or map: the number of elements in v; if v is nil, len(v) is zero. // String: the number of bytes in v. // Channel: the number of elements queued (unread) in the channel buffer; // if v is nil, len(v) is zero. // For some arguments, such as a string literal or a simple array expression, the // result can be a constant. See the Go language specification's "Length and // capacity" section for details. func len(v Type) int

rune

Go 使用 rune 型別來表示一個 Unicode 字元的碼點。為了傳輸和儲存 Unicode 字元,Go 還使用了 UTF-8 編碼方案,UTF-8 編碼方案使用變長位元組的編碼方式,碼點小的字元用較少的位元組編碼,碼點大的字元用較多位元組編碼,這種編碼方式相容 ASCII 字符集,並且擁有很高的空間利用率。

Go rune 的概念和 Java 的 char 類似,字元字面值,Unicode 字元的編碼,本質是一個整數

// $GOROOT/src/builtin.go type rune = int32

  • Unicode字符集中的中文字元:'a', '中'
  • Unicode :字元\u 或\U 作為字首: '\u4e2d'(字元:中), '\U00004e2d'(字元:中)

UTF-8 編碼方案已經成為 Unicode 字元編碼方案的事實標準,各個平臺、瀏覽器等預設均使用 UTF-8 編碼方案對 Unicode 字元進行編、解碼。

UTF-8(Go 語言之父 RobPike 和其他人聯合創造)的特點:

  • 使用變長位元組表示,1~4 個位元組不等,空間利用率高
  • 相容 ASCII 字元

為什麼 UTF-8 沒有位元組序問題?

位元組序問題:超出一個位元組的資料如何儲存的問題。是使用大端還是小端,從哪頭開始讀取合適。 因為UTF-8的頭已經標出來了,所以不存在順序出問題的情況。

UTF-8 是變長編碼,其編碼單元是單個位元組,不存在誰在高位、誰在低位的問題。而 UTF-16 的編碼單元是雙位元組,utf-32編碼單元為4位元組,均需要考慮位元組序問題。 UTF-8 通過位元組的形式就能確定傳輸的是多少位元組的字元編碼(如果是超過一個位元組表示,會在首位元組的前幾位用特殊值表示)

測試一下 rune 轉位元組陣列和位元組陣列轉 rune: ``` func TestRune() { //定義一個字元 var r rune = 0x4E2D fmt.Printf("The unicode character is: %c", r) fmt.Print("\n")

//encode
p := make([]byte, 3)    //建立一個數組
_ = utf8.EncodeRune(p, r)   //編碼為二進位制
fmt.Printf("encode result: 0x%X", p)
fmt.Print("\n")

//decode 0xE4B8AD
buf := []byte {0xE4, 0xB8, 0xAD}
r2, size := utf8.DecodeRune(buf)
fmt.Printf("decode result: %c, size:%d", r2, size)
fmt.Print("\n")

} ```

執行結果:

The unicode character is: 中 encode result: 0xE4B8AD decode result: 中, size:3

Go 字串型別的內部表示

Go string 執行時,僅僅是一個指標和長度,並不儲存真實的資料: // StringHeader is the runtime representation of a string. // It cannot be used safely or portably and its representation may // change in a later release. // Moreover, the Data field is not sufficient to guarantee the data // it references will not be garbage collected, so programs must keep // a separate, correctly typed pointer to the underlying data. type StringHeader struct { Data uintptr Len int }

指向資料的指標和一個長度值。

在這裡插入圖片描述

舉個例子: ``` func TestDumpBytes() { s := "hello" //1.獲取這個字串的地址,轉換為一個 StringHeader header := (*reflect.StringHeader)(unsafe.Pointer(&s)) fmt.Printf("0x%X\n", header.Data); //輸出底層陣列的地址

//2.通過 StringHeader 的資料指標獲取到真實的字串資料
originString := (*[5]byte)(unsafe.Pointer(header.Data)) //StringHeader.Data 就是一個指標
fmt.Printf("originString :%s\n", *originString) //通過 * 獲取指標指向的內容

//3.遍歷每個字元,列印
for _, c := range *originString {
    fmt.Printf("%c_", c)
}
fmt.Print("\n")

} ```

上面的程式碼中,我們通過 unsafe.Pointer 讀取了字串的底層實現,然後通過 String.Header 的結構體裡的資料,實現了字串的列印。

輸出資訊:

0x10C9622 originString :hello h_e_l_l_o_

字串操作

  1. 通過下標讀取,結果是位元組(而不是字元)
  2. 字元迭代(for 迭代和 for range 迭代,得到的結果分別是:位元組、字元)
  3. 字串拼接(+/+=, strings.Builder, strings.Join, fmt.Sprintf)
  4. 字串比較(==, !=, >=, <=)
  5. 字串轉換(string/byte[]/rune[])

字串拼接效能對比

  • +/+=是將兩個字串連線後分配一個新的空間,當連線字串的數量少時,兩者沒有什麼區別,但是當連線字串多時,Builder的效率要比+/+=的效率高很多。
  • 因為 string.Builder 是先將第一個字串的地址取出來,然後將builder的字串拼接到後面,

常量

https://time.geekbang.org/column/article/442791

Go 常量的創新:

  • 無型別常量:宣告時不賦予型別的常量
  • 隱式自動轉型:根據上下文把無型別常量轉為對應型別
  • 可用於實現列舉

使用 const 關鍵字,也支援類似 var 那樣的程式碼塊,宣告多個常量。

無型別常量與常量隱式轉型的組合,使得在 Go 在混合資料型別運算的時候具有比較大的靈活性,程式碼編寫也得到簡化。

Go 沒有提供列舉型別,可以使用 const 程式碼塊 + iota 實現列舉

  • iota:行偏移量指示器,表示當前程式碼塊的行號,從 0 開始
  • const 程式碼塊裡,如果沒有顯式初始化,就會複製上一行,但因為行號不一樣,所以就實現了增加

每個 const 程式碼塊都擁有屬於自己的 iota。同一行即使出現多次,多個 iota 的值都一樣

const ( _ = iota APPLE WATERMELON _ BINANA = iota + 1000 _ ORANGE ) func TestConstValue() { fmt.Printf("test enum %d \n", APPLE) fmt.Printf("test enum %d \n", WATERMELON) fmt.Printf("test enum %d \n", BINANA) fmt.Printf("test enum %d \n", ORANGE) }

輸出:

test enum 1 test enum 2 test enum 1004 test enum 1006

注意⚠️:要定義大量常量時,建議不要使用 iota,否則不清楚值到底是多少!

陣列和切片

https://time.geekbang.org/column/article/444348

陣列

``` //引數的型別,如果是陣列,個數也必須一致 func printArray(a [5]int) { fmt.Printf("length of arr: %d \n", len(a)) //元素個數 fmt.Printf("size of arr: %d \n", unsafe.Sizeof(a)) //所有元素佔用的位元組數

for i := 0; i < len(a); i++ {
    fmt.Printf("index: %d , value: %d, addr: 0x%x \n", i, a[i], &a[i])  //也可以取地址
}

}

func (Array) test() { var arr [5]int printArray(arr)
arr[0] = 1

//var arr2 []int    //不宣告預設長度為 0
//printArray(arr2)  //編譯報錯:Cannot use 'arr' (type [5]int) as type []int

//var arr3 = [5]int {1,2,3,4,5} //直接初始化,型別在等號右邊,花括號包圍初始值
//var arr3 = [...]int {1,2,3,4,5}   //也可以省略長度,編譯時推導
var arr3 = [5]int { 2: 3 }  //也可以指定下標賦值,只設置某個值
printArray(arr3)

} ```

輸出: length of arr: 5 size of arr: 40 index: 0 , value: 0, addr: 0xc00001e2d0 index: 1 , value: 0, addr: 0xc00001e2d8 index: 2 , value: 0, addr: 0xc00001e2e0 index: 3 , value: 0, addr: 0xc00001e2e8 index: 4 , value: 0, addr: 0xc00001e2f0

從上面的例子可以看出: - Go 中,陣列等價要求型別和長度一致,如果不一致,無法傳遞、賦值。 - 直接初始化:型別寫在等號右邊,花括號包圍初始值;可以省略長度,編譯時推導;也可以指定下標賦值,只設置某個值 - 陣列宣告時不賦值,會初始化元素為零值 - item 的地址是連續的

在 Java 中型別校驗這麼嚴格,陣列引數只要型別一致即可。

陣列的特點:元素個數固定;作為引數傳遞時會完全拷貝一次,記憶體佔用大。

切片

宣告時不指定大小,append 新增元素,動態擴容

Go 編譯器會為每個新建立的切片建立一個數組,然後讓切片指向它。

切片的實現: //go/src/runtime/slice.go type slice struct { array unsafe.Pointer //指向底層陣列的指標 len int cap int }

在這裡插入圖片描述

切片好比打開了一個訪問與修改陣列的“視窗”,通過這個視窗,我們可以直接操作底層陣列中的部分元素。

在這裡插入圖片描述

``` type MySlice struct {}

func printSlice(sl []int) { fmt.Printf("length: %d, capcity: %d \n", len(sl), cap(sl)) for i, i2 := range sl { fmt.Printf("(%d, %d ) ", i, i2) } fmt.Printf("\n") }

func (MySlice) test() { //1.建立切片 sl := make([]int , 5, 7) //建立了一個切片,長度為 5,有 5 個元素為 0 的值 printSlice(sl) sl = append(sl, 1,2,3,4,5,6) //新增 6 個,超出容量,翻倍,元素個數為 5 + 6 printSlice(sl)

//陣列與切片型別不相容:Cannot use '[5]int {1,2,3,4,5}' (type [5]int) as type []int
//printSlice([5]int {1,2,3,4,5})

//2.陣列的切片化
var arr = [...]int {1,2,3,4,5}
//從索引 1 (第二個)開始,長度到 2,容量到 5
sl2 := arr[1:3:5]   //長度是第二個值減去第一個,容量是第三個值減去第一個
printSlice(sl2)

sl2[0] = 444    //修改切片,會影響原始陣列
fmt.Printf("origin array first value: %d\n", arr[1])

sl2 = append(sl2, 2, 3, 4, 5)
printSlice(sl2)
sl2[0] = 555    //擴容後會建立新陣列,再修改不會影響原始陣列
fmt.Printf("origin array first value: %d\n", arr[1])

} ```

切片在做為引數傳遞時,只傳遞指標,成本更低。

切片擴容後會建立新陣列,再修改不會影響原始陣列

如何把完整的陣列轉換為切片:a[:] ,意思是將陣列 a 轉換為一個切片,長度和容量和陣列一致。

在大多數場合,我們都會使用切片以替代陣列。

map 使用及實現

https://time.geekbang.org/column/article/446032

Go map 是一個無序的 key-value 資料結構。

注意點: 1. 不要依賴 map 的元素遍歷順序; 2. map 不是執行緒安全的,不支援併發讀寫; 3. 不要嘗試獲取 map 中元素(value)的地址。

for range 過程中的 k,v 公用,不能直接做引用傳遞。


Go map 的 key 型別有要求,必須支援 == 操作符,這就導致這些型別不能做 key: - 函式 - map - 切片

切片零值可用,但 map 不支援,必須初始化後才能用。

兩種賦值方式: - 短變數的方式,加上 {},就是初始化了 - 通過 make 建立

``` type MyMap struct {}

type Position struct { x float64 y float64 }

func updateMap(m map[string]int) { m["hihi"] = 1024 }

func (MyMap) test() { //var m map[int]string //不賦值,預設為 nil。這時操作的話會報錯:panic: assignment to entry in nil map //m := map[int]string{} //1.短變數的方式,加上 {},就是初始化了 m := make(map[int]string, 6) //2.通過 make 建立 m[1] = "haha" m[1024] = "bytes" fmt.Println(m[1], len(m)) //len(map): 獲取 map 中已儲存的鍵值對

for k, v := range m {
    fmt.Printf("(%d, %s) ", k, v)
}
fmt.Printf("\n")

//第一種初始化方式:字面值初始化
//m1 := map[Position]string {   //較為複雜的初始化,寫全型別
//  Position{1,2}: "home",
//  Position{3,4 }: "company",
//}
m1 := map[Position]string {
    {1,2}: "home",  //初始化賦值時,可以省略掉型別,直接以內容作為 key
    {3,4 }: "company",
}

fmt.Println(m1)

p := Position{1,2}
m1[p] = "shop"
fmt.Println(m1)

delete(m1, p)   //通過內建函式 delete 刪除 map 的值,引數為 map 和 key
fmt.Println("after delete: ", m1)

//通過下標訪問不存在的值,會返回這個型別的零值
emptyMap := make(map[string]int)
fmt.Printf("try key that is not inside map: %s , %d\n", m[1024], emptyMap["hihi"])

//map 作為引數是引用傳遞,內部修改,外部也有影響!
updateMap(emptyMap)
value, ok := emptyMap["hihi"]   //通過 _value, ok(逗號 + ok) 的方式判斷是否存在於 map
if !ok {
    fmt.Println("hihi not in map")
} else {
    fmt.Println("hihi in the map! ", value)
}

} ```

輸出:

haha 2 (1, haha) (1024, bytes) map[{1 2}:home {3 4}:company] map[{1 2}:shop {3 4}:company] after delete: map[{3 4}:company] try key that is not inside map: bytes , 0 hihi in the map! 1024

可以看到:

  • 對 map 遍歷多次時,元素的次序不確定,可能會有變化。
  • 和 切片 一樣,map 也是引用型別。
  • map 作為引數是引用傳遞,內部修改,外部也有影響!⚠️
  • 在 map 中查詢和讀取時,建議通過逗號 + ok 的方式,以確認 key 是否存在!

map 內部實現

map 型別在 Go 執行時層實現的示意圖:

在這裡插入圖片描述

圖片來自:https://time.geekbang.org/column/article/446032

  • hmap: runtime.hmap,map 型別的頭部結構(header)
  • bucket:真正儲存鍵值對資料的資料結構,雜湊值低位相同的元素會放到一個桶裡,一個桶預設 8 個元素
  • overflow bucket:某個 bucket元素 > 8 && map 不需要擴容時,會建立這個溢位桶,資料儲存在這裡

[2.hmap 資料介紹圖]

每個 bucket 由三部分組成: 1. tophash 2. key 3. value

雜湊值分兩部分: - 低位值是桶索引,決定當前訪問資料在第幾個桶 - 高位值是桶內索引(tophash 陣列的索引),決定在桶裡第幾個

使用雜湊值,可以提升這兩步查詢時的速度。

【3.雜湊值圖】

Go 執行時採用了把 key 和 value 分開儲存的方式,而不是採用一個 kv 接著一個 kv 的 kv 緊鄰方式儲存,這帶來的其實是演算法上的複雜性,但卻減少了因記憶體對齊帶來的記憶體浪費。

【4.記憶體佔用對比圖】

```

// $GOROOT/src/runtime/map.go const ( // Maximum number of key/elem pairs a bucket can hold. bucketCntBits = 3 bucketCnt = 1 << bucketCntBits

// Maximum average load of a bucket that triggers growth is 6.5.
// Represent as loadFactorNum/loadFactorDen, to allow integer math.
loadFactorNum = 13
loadFactorDen = 2

// Maximum key or elem size to keep inline (instead of mallocing per element).
// Must fit in a uint8.
// Fast versions cannot handle big elems - the cutoff size for
// fast versions in cmd/compile/internal/gc/walk.go must be at most this elem.
maxKeySize  = 128
maxElemSize = 128

) ```

map 擴容的兩個場景: 1. map 的元素個數 > LoadFactor * 2^B 2. overflow bucket 過多時

目前 Go 最新 1.17 版本 LoadFactor 設定為 6.5 (loadFactorNum/loadFactorDen)

```

// Like mapaccess, but allocates a slot for the key if it is not present in the map. func mapassign(t maptype, h hmap, key unsafe.Pointer) unsafe.Pointer { // If we hit the max load factor or we have too many overflow buckets, // and we're not already in the middle of growing, start growing. if !h.growing() && (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) { hashGrow(t, h) goto again // Growing the table invalidates everything, so try again } }

// overLoadFactor reports whether count items placed in 1< bucketCnt && uintptr(count) > loadFactorNum*(bucketShift(B)/loadFactorDen) }

// tooManyOverflowBuckets reports whether noverflow buckets is too many for a map with 1< 15 { B = 15 } // The compiler doesn't see here that B < 16; mask B to generate shorter shift code. return noverflow >= uint16(1)<<(B&15) } ```

如果是元素個數超出負載因子,會建立一個 2 倍大小的桶陣列,原始桶資料會儲存在 hmapoldbuckets 下,直到所有原始桶資料複製到新陣列。

【5. hmap oldbuckets 圖片】

map 例項不是併發寫安全的,也不支援併發讀寫。如果我們對 map 例項進行併發讀寫,程式執行時就會丟擲異常。如果要併發讀寫 map,可以使用 sync.Map

struct

https://time.geekbang.org/column/article/446840

型別定義也支援一次性定義多個:

``` type ( T1 int T2 T1 )

type T struct { my T // compile error: Invalid recursive type 'MyStruct' t *T // ok st []T // ok m map[string]T // ok } ```

空結構體記憶體佔用為 0:

var em EmptyStruct fmt.Println("size of empty struct: ", unsafe.Sizeof(em)) //size of empty struct: 0

空結構體型別的變數不佔用記憶體空間,十分適合作為一種“事件”在併發的 Goroutine 間傳遞。

Go struct 是零值可用的,可以聲明後就直接使用。

同時也支援宣告值時通過字面值初始化,共有這幾種方式:

  1. 按順序賦值:p := Position{1,2}
  2. 通過指定引數名稱進行賦值:q := Position{y: 3, x: 6}

如果一個結構體的構造比較複雜,傳入引數後需要內部做很多工作,我們可以為結構體型別定義一個函式,用於構造這個結構體:

func NewT(field1, field2, ...) *T { ... ... }

例如:

func NewPosition(center float64) *Position { return &Position{ x: center - 10, y: center + 10, } }

struct 的記憶體佈局

在執行時,Go 結構體的元素(成員、方法)存放在一個連續記憶體塊中。

記憶體對齊,是出於提升處理器存取資料效率的考慮。

對於各種基本資料型別來說,它的變數的記憶體地址值必須是其型別本身大小的整數倍。

比如:一個 int64 型別的變數的記憶體地址,應該能被 int64 型別自身的大小,也就是 8 整除;一個 uint16 型別的變數的記憶體地址,應該能被 uint16 型別自身的大小,也就是 2 整除。

為了合理使用記憶體,編譯器可能會給結構體中填充資料(和 C/C++ 類似),包括兩種: 1. 欄位填充:讓欄位的地址可以被自己的型別佔用位元組數整除 2. 尾部填充:保證每個結構體變數的地址是一個值的整數倍,這個值是 結構體內最長欄位的長度 和 系統記憶體對齊係數 的最小值

64 位處理器上,記憶體對齊係數一般為 8。

在這裡插入圖片描述

我們開發者在定義結構體時,儘量合理安排欄位順序,否則填充過多會導致佔用空間更大。


  • Go 中迴圈語句只有 for,沒有 while 和 do while
  • Go 中的 switch,型別也可以作為條件;case 不需要寫 break

if 自用變數

https://time.geekbang.org/column/article/447723

操作符優先順序:

【1.操作符優先順序表】

注意:邏輯與和邏輯或的優先順序很低,如果在一個條件語句裡有其他操作符,邏輯與和或最後執行。

比如這個例子:

func main() { a, b := false,true if a && b != true { println("(a && b) != true") return } println("a && (b != true) == false") }

第一直覺是先執行 a && b。⚠️ 實際上,!= 比 && 優先順序更高,所以先執行 !=。

if 語句的自用變數:在布林表示式前宣告的變數,作用範圍只在 if 程式碼塊中(包括 else 裡)。

第一直覺是隻在宣告的程式碼塊裡,⚠️ 實際上,else 裡也可以訪問到。

``` //if 的自用變數,在 if 的 else 程式碼塊裡也可以訪問 if tempA := 1; tempA > 0 { //... } else { fmt.Println(tempA) //仍然可以訪問 }

fmt.Println(tempA)  //Unresolved reference 'tempA'

```

為什麼多個條件邏輯中,最容易命中的寫到前面,是最好的呢? 這裡面主要涉及到2個技術點:流水線技術和分支預測 流水線技術:簡單的說,一條 CPU 指令的執行是由 取指令-指令譯碼-指令執行-結果回寫組成的(簡單的說哈,真實的流水線是更長更復雜的);第一條指令譯碼的時候,就可以去取第二條指令,因此可以通過流水線技術提高CPU的使用率。 分支預測:如果沒有任何分支預測,那麼就是按照程式的程式碼順序執行,那麼執行到if上一句的時候,指令譯碼就是if語句,取指令就是if語句塊的第一句,那麼if如果不滿足的話,就會執行JMP指令,跳轉到else,因此流水線中的取指令與指令譯碼其實是無用功。因此在沒有任何分支預測優化的情況下,if語句需要把概率更高的條件寫到最上面,更能體現流水線的威力。 現代計算機都有分支預測的優化,比如動態分支預測等技術,但是不管怎麼說,把概率最大的放到最上面,還是很有必要的。

在C語言中,有類似這樣的巨集定義,可以使用 __builtin_expect函式,主動提示那個分支的程式碼的概率更高

迴圈的新花樣和坑

與其他語言相比,Golang 中的迴圈有幾種新方式:

``` var i int for ; i< 10; { //1.省略前置和後置 i++ }

for i< 10 {     //2.連冒號也省略
    i++
}

for {           //3.沒條件(類似 while(true))
    if i < 10 {

    } else {
        break
    }
}

s := []int{}
for range s {   //4.不關心下標和值,可以省略
    //...
}

```

⚠️ for range 的一些點:

  • 使用 for 經典形式與使用 for range ,對 string 型別來說有所區別(for range string 時,每次迴圈得到的 v 值是一個 Unicode 字元碼點,也就是 rune 型別值,而不是一個位元組);
  • for range 是遍歷 map 的唯一方式;
  • for range channel 時,會阻塞在 channel 的讀操作上,直到 channel 關閉後 for range 才會結束。

Go 的 continue 支援跳轉到某個 label 處(一般用於巢狀迴圈)。

⚠️ 第一直覺這個和 C/C++ 的 goto (Go 也支援 goto)類似。但實際上還是差別很大的,goto 跳轉後會重新開始,continue 還會繼續之前的邏輯。

Go 的 break 也支援 label,用於終結 label 對應的迴圈。

舉個例子: outer: for i := 0; i < 3; i++ { fmt.Println("outer loop: ", i) for j := 0; j < 6; j++ { if j == 2 { //break outer //結束 outer 冒號後的最外層迴圈 continue outer //繼續執行外層迴圈 //continue //跳過 2 } fmt.Println("inner loop: ", i, j) } }

for range 容易踩的 3 個坑

1.迴圈變數的重用

⚠️ 第一直覺會覺得 for range 每次迭代都會重新宣告兩個新的變數 i 和 v。但事實上,i 和 v 在 for range 語句中僅會被v宣告一次,且在每次迭代中都會被重用。

for i, v := range m { //... }

等價於:

i, v := 0, 0 //只建立一次 for i, v = range m { //每次修改這個值 //... //如果有延遲執行的程式碼,可能訪問到的是 i, v 的最終結果 }

別人踩的坑:go for 迴圈時range埋下的坑好大

2.迴圈裡使用的是一個副本,修改不影響原值

```

var arr = [...]int {1,2,3,4,5}
var copy [5]int

fmt.Println("origin arr ", arr)

for i, v := range arr { 
    if i == 0 {
        arr[1] = 100    //這裡修改的是原始的
        arr[2] = 200
    }
    fmt.Println("in for-range ", i, v)
    copy[i] = v
}

fmt.Println("after loop, arr ", arr)
fmt.Println("after loop, copy ", copy)

```

第一直覺會覺得在第一次迴圈執行時修改了 arr,後面迴圈裡的值也會變,結果打臉了。輸出結果:

origin arr [1 2 3 4 5] in for-range 0 1 in for-range 1 2 in for-range 2 3 in for-range 3 4 in for-range 4 5 after loop, arr [1 100 200 4 5] after loop, copy [1 2 3 4 5]

⚠️ for i, v := range arr 這行第一次執行時時會拷貝一份,後面每次迭代,取的是拷貝的資料。 等價於:for i, v := range arrCopy,所以在程式碼塊裡修改 arr 不影響後續迭代內容。

把 range 的物件改為 arr[:] 就會輸出期望的內容,因為 arr[:] 的意思是基於 arr 陣列建立一個切片,這個切片的長度和容量和 arr 一致,然後修改切片內容時,會修改底層陣列 arr 的內容。

3.迴圈遍歷 map 時,獲得的結果順序不一定

很簡單的 for-range 遍歷 map,但每次執行結果就是不一樣!

``` var m = map[string]int { "shixin": 1, "hm" : 2, "laotang" : 3, "leizi" : 4, }

counter := 0
for i, v := range m {

// if counter == 0 { // delete(m, "laotang") // } counter++ fmt.Println("item ", i, v) } ```

具體原因還需要深入看下相關程式碼,有需求再看,先記住結論吧!

由於 map 和切片一樣,都是用一個指標指向底層資料,所以在 for range 迴圈裡修改 map,會影響到原始資料。

所以如果有在迴圈裡修改 map 資料的操作,可能會有預期外的結果,慎重!!

switch 和其他語言有點小區別

https://time.geekbang.org/column/article/455912

和 C/Java 相比 Go switch 的特點是: - 支援表示式列表:case a, b, c, d: - 每個 case 分支的程式碼塊執行完就結束 switch,不需要 break - case 語句中可以通過 fallthrough 執行下一個 case - 支援 type switch,型別判斷

switch counter { case 4: fmt.Println("counter is 4") fallthrough //仍然可以執行下一個 case case 77: fmt.Println("counter is 77") case 770: fmt.Println("counter is 770") default: fmt.Println("switch default") }

由於 fallthrough 的存在,Go 不會對下一個 case 的表示式做求值操作,而會直接執行下一個 case 對應的程式碼分支

不帶 label 的 break 中斷的是所在的最內層的 for、switch 或 select。如果想中斷多層巢狀(比如 for 裡巢狀 switch),需要通過 break label。

type switch

Go switch 支援了 Java 沒有的型別:type switch,就是判斷型別。使用時在 switch 後寫 變數.(type),比如:

var i interface{} = 13 switch i.(type) { //變數.(type),判斷型別 case nil: fmt.Println("type is nil", i) case int: fmt.Println("type is int", i) case interface{}: fmt.Println("type is interface", i) }

輸出:

type is int 13

⚠️ 只有介面型別變數才能使用 type switch,並且所有 case 語句中的型別必須實現 switch 關鍵字後面變數的介面型別

實踐收穫記錄

  1. postgresql 佔位符是 $1 $2,mysql 是 ?
  2. 生成 linux 平臺可執行檔案:CGO_ENABLED=0 GOOS=linux go build

學習資料

官方文件: - https://go.dev/doc/ - https://github.com/golang/go/wiki

一個比較全的學習教程:https://www.liwenzhou.com/posts/Go/golang-menu/