文章目錄
- 初步了解Go語言
- Go語言誕生的主要問題和目標
- Go語言應用典型代表
- Go語言開發環境搭建
- 經典HelloWorld
- 基本程序結構編寫學習
- 變量
- 常量
- 數據類型
- 運算符
- 條件語句
- if語句
- switch 語句
- 跳轉語句
- 常用集合和字符串
- 數組
- 切片
- Map
- 實現Set
- **字符串**
- 函數
- **基本使用用例驗證**
- 面向對象編程
- **結構體的定義**
- **實例創建及初始化**
- 行為(方法)定義
- 接口定義使用
- **擴展和復用**
- 空接口和斷言
- GO 接口最佳實踐
- 錯誤類型
- 基本使用介紹
- 錯誤鏈
- Panic 和 Recover
- 自定義錯誤類型
- 包和依賴管理
- `package`(包)的基本知識點
- 構建一個自身可復用的`package`
- 導入和應用遠程依賴(即外部包)
- 包的依賴管理
- 導入和應用遠程依賴(即外部包)
- 包的依賴管理
初步了解Go語言
Go語言誕生的主要問題和目標
多核硬件架構: 隨著計算機硬件的發展,多核處理器成為主流,使得并行計算變得普遍。然而,傳統的編程語言在處理多核并行性時可能面臨困難,因為它們缺乏合適的原生支持。Go語言通過引入輕量級的協程(goroutine)和通道(channel)機制,使得并發編程變得更加容易。開發者可以輕松地創建數千個并發執行的協程,而無需擔心線程管理的復雜性。
超大規模分布式計算集群: 隨著云計算和分布式系統的崛起,構建和維護超大規模的分布式計算集群變得越來越常見。這些集群需要能夠高效處理大量的請求、數據共享和協調。Go語言的并發特性和通道機制使得編寫分布式系統變得更加容易,開發者可以使用協程和通道來處理并發任務、消息傳遞和協調工作。
Web模式導致的開發規模和更新速度增加: Web應用的興起帶來了前所未有的開發規模和持續更新的需求。傳統的編程語言在開發大型Web應用時可能會面臨可維護性、性能和開發效率等問題。Go語言通過其簡潔的語法、高效的編譯速度以及并發支持,使得開發者能夠更快速地迭代和部署Web應用,同時也能夠更好地處理高并發的網絡請求。
綜合來看,Go語言在誕生時確實著重解決了多核硬件架構、超大規模分布式計算集群和Web模式下的開發規模與速度等技術挑戰,它的設計目標之一是提供一種適應現代軟件開發需求的編程語言,使開發者能夠更好地應對這些挑戰。
Go語言應用典型代表
Go語言在當下應用開發中已經得到廣泛應用,許多知名公司和項目都使用Go語言來構建各種類型的應用。以下是一些代表性的產品和項目,它們使用了Go語言作為核心開發語言:
這些僅僅是Go語言應用的一小部分示例,實際上還有許多其他的項目和產品也在使用Go語言來構建高性能、可靠且易于維護的應用程序。這表明Go語言在現代應用開發中發揮了重要作用,特別是在分布式系統、云計算和高性能應用領域。
Go語言開發環境搭建
請參考該文章進行搭建:
Go語言開發環境搭建
經典HelloWorld
src目錄下新建hello.go文件,編寫代碼如下:
package mainimport ("fmt""os"
)func main() {if len(os.Args) > 1 {fmt.Println("hello world", os.Args[1])}
}
這段代碼是一個簡單的Go語言程序,它接受命令行參數并打印出一條帶參數的 “Hello World” 消息。下面是對代碼的逐行分析:
package main
: 聲明這個文件屬于名為 “main” 的包,這是一個Go程序的入口包名。
import ("fmt" "os")
: 引入了兩個標準庫包,分別是 “fmt” 用于格式化輸出,和 “os” 用于與操作系統交互。
func main() { ... }
: 這是程序的入口函數,它會在程序運行時首先被調用。
if len(os.Args) > 1 { ... }
: 這個條件語句檢查命令行參數的數量是否大于1,也就是判斷是否有參數傳遞給程序。os.Args
是一個字符串切片,它包含了所有的命令行參數,第一個參數是程序的名稱。
fmt.Println("Hello World", os.Args[1])
: 如果有參數傳遞給程序,就會執行這行代碼。它使用 fmt.Println
函數打印一條消息,消息由字符串 “Hello World” 和 os.Args[1]
組成,os.Args[1]
表示傳遞給程序的第一個參數。
綜上所述,這段代碼涵蓋了以下知識點:
包導入和使用標準庫:通過 import
關鍵字導入 “fmt” 和 “os” 包,然后在代碼中使用這些包提供的函數和類型。
命令行參數獲取:使用 os.Args
獲取命令行參數。
條件語句:使用 if
條件語句來判斷是否有命令行參數傳遞給程序。
字符串操作:使用字符串連接操作將 “Hello World” 與命令行參數拼接在一起。
格式化輸出:使用 fmt.Println
函數將消息輸出到標準輸出。
注意:如果沒有傳遞參數給程序,那么這段代碼不會打印任何消息。如果傳遞了多個參數,代碼只會使用第一個參數并忽略其他參數。
在該目錄下執行“go run hello.go hzy”,運行結果為“hello world hzy”。
PS D:\Microsoft VS Code\GOproject\src\go_code> go run .\1.go hzy
hello world hzy
PS D:\Microsoft VS Code\GOproject\src\go_code>
基本程序結構編寫學習
變量
前提:chapter2目錄下創建variables,學習總結如下:
- 變量聲明: 使用
var
關鍵字聲明一個變量,例如:var x int
。 - 類型推斷: 可以使用
:=
操作符進行變量聲明和賦值,Go會根據右側的值自動推斷變量類型,例如:y := 5
。 - 變量賦值: 使用賦值操作符
=
給變量賦值,例如:x = 10
。 - 多變量聲明: 可以同時聲明多個變量,例如:
var a, b, c int
。 - 變量初始化: 變量可以在聲明時進行初始化,例如:
var name string = "John"
。 - 零值: 未初始化的變量會被賦予零值,數字類型為0,布爾類型為
false
,字符串類型為空字符串等。 - 短變量聲明: 在函數內部,可以使用短變量聲明方式,例如:
count := 10
。
新建fib_test.go,背景:簡單實用斐波那契數列進行練習
package variablesimport "testing"func TestFibList(t *testing.T) {a := 1b := 1t.Log(a)for i := 0; i < 5; i++ {t.Log(" ", b)tmp := aa = bb = tmp + a}
}func TestExchange(t *testing.T) {a := 1b := 2// tmp := a// a = b// b = tmpa, b = b, at.Log(a, b)
}
下面逐個解釋代碼中涉及的知識點:
package variables
: 聲明了一個名為 “variables” 的包,這是一個用于測試的包名。
import "testing"
: 導入了Go語言的測試框架 “testing” 包,用于編寫和運行測試函數。
func TestFibList(t *testing.T) { ... }
: 定義了一個測試函數 “TestFibList”,該函數用于測試斐波那契數列生成邏輯。這是一個測試函數的標準命名,以 “Test” 開頭,接著是被測試的函數名。
在測試函數內部,聲明了兩個整數變量 a
和 b
,并將它們初始化為 1,這是斐波那契數列的前兩個數。使用 t.Log(a)
打印變量 a
的值到測試日志中。使用循環來生成斐波那契數列的前 5 個數,每次迭代都會將 b
的值打印到測試日志,并更新 a
和 b
的值以生成下一個數。
func TestExchange(t *testing.T) { ... }
: 定義了另一個測試函數 “TestExchange”,該函數用于測試變量交換的邏輯。
在測試函數內部,聲明了兩個整數變量 a
和 b
,并分別將它們初始化為 1 和 2。使用注釋的方式展示了一種變量交換的寫法(通過中間變量),但實際上被注釋掉了。然后使用 a, b = b, a
這一行代碼來實現 a
和 b
的交換,這是Go語言中的一種特有的交換方式,不需要額外的中間變量。使用 t.Log(a, b)
打印交換后的變量值到測試日志中。
輸出測試結果:
Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestFibList$ go_code/chapter/1=== RUN TestFibListd:\Microsoft VS Code\GOproject\src\go_code\chapter\1\variables_test.go:8: 1d:\Microsoft VS Code\GOproject\src\go_code\chapter\1\variables_test.go:10: 1d:\Microsoft VS Code\GOproject\src\go_code\chapter\1\variables_test.go:10: 2d:\Microsoft VS Code\GOproject\src\go_code\chapter\1\variables_test.go:10: 3d:\Microsoft VS Code\GOproject\src\go_code\chapter\1\variables_test.go:10: 5d:\Microsoft VS Code\GOproject\src\go_code\chapter\1\variables_test.go:10: 8
--- PASS: TestFibList (0.00s)
PASS
ok go_code/chapter/1 0.066s
Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestExchange$ go_code/chapter/1=== RUN TestExchanged:\Microsoft VS Code\GOproject\src\go_code\chapter\1\variables_test.go:24: 2 1
--- PASS: TestExchange (0.00s)
PASS
ok go_code/chapter/1 0.065s
常量
-
常量聲明: 使用
const
關鍵字聲明一個常量,例如:const pi = 3.14159
。 -
常量賦值: 常量的值在聲明時必須被賦值,一旦賦值后不可修改。
-
枚舉常量: 可以使用一組常量來模擬枚舉。
-
類型指定: 常量的類型也可以被指定,例如:
const speed int = 300000
。 -
常量表達式: 常量可使用表達式計算,例如:
const secondsInHour = 60 * 60
。 -
無類型常量: 常量可以是無類型的,根據上下文自動推斷類型。例如,
const x = 5
會被推斷為整數類型。
//枚舉常量:
const (TuesdayWednesday
)
例子:
package constantimport "testing"const (Monday = 1 + iotaTuesdayWednesday
)// 第一個常量塊中,使用了 iota 常量生成器來定義了一系列從 1 開始遞增的常量。
// 在這個例子中,Monday 被賦值為 1,Tuesday 被賦值為 2,Wednesday 被賦值為 3。
// iota 在常量塊中每次被使用時會遞增一次,因此后續的常量會依次遞增;
//第二個常量塊中,使用了 iota 來定義了一系列按位左移的常量。
// 在這個例子中,Readable 被賦值為 1,Writable 被賦值為 2(二進制中的 10),Executable 被賦值為 4(二進制中的 100)。
// 位運算中,左移操作可以將二進制數向左移動指定的位數。const (Readable = 1 << iotaWriteableExecutable
)func TestCostant1(t *testing.T) {t.Log(Monday, Tuesday)
}func TestCostant2(t *testing.T) {a := 1t.Log(a&Readable == Readable, a&Writeable == Writeable, a&Executable == Executable)
}
package constant
: 聲明了一個名為 “constant” 的包,這是一個用于測試的包名。
import "testing"
: 導入了Go語言的測試框架 “testing” 包,用于編寫和運行測試函數。
const (...)
: 定義了兩個常量塊。
第一個常量塊中,使用了 iota
常量生成器來定義了一系列從 1 開始遞增的常量。在這個例子中,Monday
被賦值為 1,Tuesday
被賦值為 2,Wednesday
被賦值為 3。iota
在常量塊中每次被使用時會遞增一次,因此后續的常量會依次遞增;第二個常量塊中,使用了 iota
來定義了一系列按位左移的常量。在這個例子中,Readable
被賦值為 1,Writable
被賦值為 2(二進制中的 10),Executable
被賦值為 4(二進制中的 100)。位運算中,左移操作可以將二進制數向左移動指定的位數。
func TestConstant1(t *testing.T) { ... }
: 定義了一個測試函數 “TestConstant1”,用于測試第一個常量塊中定義的常量。
使用 t.Log(Monday, Tuesday)
打印常量 Monday
和 Tuesday
的值到測試日志中。
func TestConstant2(t *testing.T) { ... }
: 定義了另一個測試函數 “TestConstant2”,用于測試位運算和常量的使用。
在測試函數內部,聲明了一個整數變量 a
,并將其初始化為 1,即二進制中的 0001。使用位運算和按位與操作來檢查變量 a
是否具有 Readable
、Writable
和 Executable
屬性。例如,a&Readable == Readable
表達式檢查 a
的二進制表示是否含有 Readable
標志位。使用 t.Log()
打印三個表達式的結果到測試日志中。
測試輸出:
Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestCostant1$ go_code/chapter/2=== RUN TestCostant1d:\Microsoft VS Code\GOproject\src\go_code\chapter\2\constant_test.go:24: 1 2
--- PASS: TestCostant1 (0.00s)
PASS
ok go_code/chapter/2 0.076s
Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestCostant2$ go_code/chapter/2=== RUN TestCostant2d:\Microsoft VS Code\GOproject\src\go_code\chapter\2\constant_test.go:29: true false false
--- PASS: TestCostant2 (0.00s)
PASS
ok go_code/chapter/2 0.068s
數據類型
前提:chapter2目錄下創建 type,學習總結如下:
Go語言具有豐富的內置數據類型,這些數據類型用于表示不同類型的值和數據。以下是對Go語言中一些主要數據類型的總結分析:
-
**整數類型(Integer Types):**Go語言提供不同大小的整數類型,如
int
、int8
、int16
、int32
和int64
。無符號整數類型有uint
、uint8
、uint16
、uint32
和uint64
。整數類型的大小取決于計算機的架構,例如32位或64位。 -
**浮點數類型(Floating-Point Types):**Go語言提供
float32
和float64
兩種浮點數類型,分別對應單精度和雙精度浮點數。 -
**復數類型(Complex Types):**Go語言提供
complex64
和complex128
兩種復數類型,分別對應由兩個浮點數構成的復數。 -
**布爾類型(Boolean Type):**布爾類型用于表示真(
true
)和假(false
)的值,用于條件判斷和邏輯運算。 -
**字符串類型(String Type):**字符串類型表示一系列字符。字符串是不可變的,可以使用雙引號
"
或反引號`
來定義。 -
**字符類型(Rune Type):**字符類型
rune
用于表示Unicode字符,它是int32的別名。通常使用單引號'
來表示字符,如'A'
。 -
**數組類型(Array Types):**數組是具有固定大小的同類型元素集合。聲明數組時需要指定元素類型和大小。
-
**切片類型(Slice Types):**切片是對數組的一層封裝,是動態長度的可變序列。切片不保存元素,只是引用底層數組的一部分。
-
**映射類型(Map Types):**映射是鍵值對的無序集合,用于存儲和檢索數據。鍵和值可以是任意類型,但鍵必須是可比較的。
-
**結構體類型(Struct Types):**結構體是一種用戶定義的復合數據類型,可以包含不同類型的字段,每個字段有一個名字和類型。
-
**接口類型(Interface Types):**接口是一種抽象類型,用于定義一組方法。類型實現了接口的方法集合即為實現了該接口。
-
**函數類型(Function Types):**函數類型表示函數的簽名,包括參數和返回值類型。函數可以作為參數傳遞和返回。
-
**通道類型(Channel Types):**通道是用于在協程之間進行通信和同步的一種機制。通道有發送和接收操作。
-
**指針類型(Pointer Types):**指針類型表示變量的內存地址。通過指針可以直接訪問和修改變量的值。
Go語言的數據類型具有清晰的語法和語義,支持豐富的內置功能。合理選擇和使用不同的數據類型可以提高程序的效率和可讀性。
類型示例使用:
package mainimport ("fmt"
)type Person struct {FirstName stringLastName stringAge int
}type Shape interface {Area() float64
}type Circle struct {Radius float64
}func (c Circle) Area() float64 {return 3.14 * c.Radius * c.Radius
}func add(a, b int) int {return a + b
}func subtract(a, b int) int {return a - b
}type Operation func(int, int) int//類型使用方法
func main() {fmt.Println("整數類型(Integer Types)")var x int = 10var y int64 = 100fmt.Println(x)fmt.Println(y)fmt.Println("浮點數類型(Floating-Point Types)")var a float32 = 3.14var b float64 = 3.1415926fmt.Println(a)fmt.Println(b)fmt.Println("布爾類型(Boolean Type)")var isTrue bool = truevar isFalse bool = falsefmt.Println(isTrue)fmt.Println(isFalse)fmt.Println("字符串類型(String Type)")str1 := "Hello,"str2 := "Go!"concatenated := str1 + str2fmt.Println(concatenated)fmt.Println("切片類型(Slice Types)")numbers := []int{1, 2, 3, 4, 5}fmt.Println(numbers)//修改切片numbers[0] = 10fmt.Println(numbers)//切片操作subSlice := numbers[1:4]fmt.Println(subSlice)fmt.Println("映射類型(Map Types)")ages := map[string]int{"hzy": 19,}fmt.Println(ages)fmt.Println("hzy ages = ", ages["hzy"])//添加新的鍵值對ages["ronaldo"] = 41fmt.Println(ages)fmt.Println("結構體類型(Struct Type)")person := Person{FirstName: "zhangsan",LastName: "Lis",Age: 30,}fmt.Println(person)fmt.Println("Name",person.FirstName, person.LastName)fmt.Println("接口類型(Interface Type)")var shape Shapecircle := Circle{Radius: 5}shape = circlefmt.Println("Circle Area", shape.Area())fmt.Println("函數類型(Function type)")var op Operationop = addresult := op(10, 5)fmt.Println("Addition:", result)fmt.Println("通道類型(Channel Type)")messages := make(chan string)go func() {messages <- "Hello Go"}()msg := <-messagesfmt.Println(msg)fmt.Println("指針類型(Pointer Types)")x = 10var ptr *intptr = &xfmt.Println("Value of x", x)fmt.Println("Value stored in pointer:", *ptr)*ptr = 20fmt.Println("Updated value of x:", x)
}
下面逐個解釋代碼中涉及的知識點:
type Person struct { ... }
: 定義了一個結構體類型 Person
,表示一個人的信息,包括 FirstName
、LastName
和 Age
字段。
type Shape interface { ... }
: 定義了一個接口類型 Shape
,該接口要求實現一個方法 Area()
返回一個 float64
類型
type Circle struct { ... }
: 定義了一個結構體類型 Circle
,表示一個圓的半徑。
func (c Circle) Area() float64 { ... }
:為 Circle
類型實現了 Shape
接口的 Area()
方法,用于計算圓的面積。
func add(a, b int) int { ... }
: 定義了一個函數 add
,用于執行整數相加操作。
func subtract(a, b int) int { ... }
: 定義了一個函數 subtract
,用于執行整數相減操作。
type Operation func(int, int) int
: 定義了一個函數類型 Operation
,它接受兩個整數參數并返回一個整數結果。
main() { ... }
: 程序的入口函數。
- 定義了多種不同類型的變量,包括整數、浮點數、布爾、字符串、切片、映射、結構體、接口、函數、通道和指針類型。
- 演示了不同類型變量的初始化、賦值、訪問以及基本操作。
- 使用切片操作提取部分切片。
- 演示了映射的使用,包括添加新的鍵值對和訪問鍵值對。
- 演示了結構體的定義和初始化,并訪問結構體字段。
- 展示了接口的使用,將
Circle
類型賦值給Shape
類型變量,并調用接口方法。 - 演示了函數類型的定義和使用,將不同函數賦值給
Operation
類型變量,并進行調用。 - 使用通道來實現并發通信,通過匿名函數在 goroutine 中發送和接收消息。
- 演示了指針的使用,包括創建指針變量、通過指針修改變量的值等操作。
輸出結果:
PS D:\Microsoft VS Code\GOproject\src\go_code\chapter\numbertype> go run .\number.go
整數類型(Integer Types)
10
100
浮點數類型(Floating-Point Types)
3.14
3.1415926
布爾類型(Boolean Type)
true
false
字符串類型(String Type)
Hello,Go!
切片類型(Slice Types)
[1 2 3 4 5]
[10 2 3 4 5]
[2 3 4]
映射類型(Map Types)
map[hzy:19]
hzy ages = 19
map[hzy:19 ronaldo:41]
結構體類型(Struct Type)
{zhangsan Lis 30}
Name zhangsan Lis
接口類型(Interface Type)
Circle Area 78.5
函數類型(Function type)
Addition: 15
通道類型(Channel Type)
Hello Go
指針類型(Pointer Types)
Value of x 10
Value stored in pointer: 10
Updated value of x: 20
Go語言中類型轉換說明
Go語言支持類型轉換,但需要注意一些規則和限制。類型轉換用于將一個數據類型的值轉換為另一個數據類型,以便在不同的上下文中使用。以下是有關Go語言中類型轉換的一些重要信息:
基本類型之間的轉換: 可以在基本數據類型之間進行轉換,但是必須注意類型的兼容性和可能導致的數據丟失。例如,從int到float64的轉換是安全的,但從float64到int可能導致小數部分被截斷。
顯示類型轉換: 在Go中,使用強制類型轉換來顯式指定將一個值轉換為另一個類型。語法是:destinationType(expression)。例如:float64(10)。
非兼容類型之間的轉換: 對于不兼容的類型,編譯器不會自動進行轉換。例如,不能直接將一個string類型轉換為int類型。
類型別名的轉換: 如果有類型別名(Type Alias),在轉換時需要注意使用別名的兼容性。
package mainimport "fmt"func main() {//顯示類型轉換var x int = 10var y float64 = float64(x)fmt.Println(y)type Celsius float64type Fahrenheit float64c := Celsius(25)f := Fahrenheit(c*9/5 + 32)fmt.Println(f)
}
輸出結果:
Running] go run "d:\Microsoft VS Code\GOproject\src\go_code\chapter\text.go"
10
77
運算符
前提:chapter2目錄下創建 operator,學習總結如下:
其實這部分和其他語言都差不多,個人覺得沒啥可復習鞏固的。Go語言支持多種運算符,用于執行各種算術、邏輯和比較操作。
以下是一些常見的運算符及其在Go中的使用方式和知識點:
常規運算符:
算術運算符(Arithmetic Operators):
+
:加法-
:減法*
:乘法/
:除法%
:取模(取余數)
賦值運算符(Assignment Operators):
=
:賦值+=
:加法賦值-=
:減法賦值*=
:乘法賦值/=
:除法賦值%=
:取模賦值
邏輯運算符(Logical Operators):
&&
:邏輯與(AND)||
:邏輯或(OR)!
:邏輯非(NOT)
比較運算符(Comparison Operators):
==
:等于!=
:不等于<
:小于>
:大于<=
:小于等于>=
:大于等于
位運算符(Bitwise Operators):
&
:按位與(AND)|
:按位或(OR)^
:按位異或(XOR)<<
:左移>>
:右移
其他運算符:
&
:取地址運算符*
:指針運算符++
:自增運算符--
:自減運算符
在使用運算符時,需要考慮以下幾點:
- 運算符的操作數必須與運算符的預期類型匹配。
- 某些運算符具有更高的優先級,需要使用括號來明確優先級。
- 運算符的操作數可以是變量、常量、表達式等。
全流程測試:
package mainimport ("fmt""testing"
)const (Readable = 1 << iotaWriteableExecutable
)func TestOperatorBasic(t *testing.T) {//算數運算符a := 10b := 5fmt.Println("Sum:", a+b)fmt.Println("Difference:", a-b)fmt.Println("Product:", a*b)fmt.Println("Quotient:", a/b)fmt.Println("Remainder", a%b)//邏輯運算符x := truey := falsefmt.Println("AND:", x && y)fmt.Println("OR:", x || y)fmt.Println("NOT:", !x)//比較運算符fmt.Println("Equal:", a == b)fmt.Println("Not Equal:", a != b)fmt.Println("Greater Than:", a > b)fmt.Println("Less Than:", a < b)fmt.Println("Greater Than or Equal:", a >= b)fmt.Println("Less Than or Equal:", a <= b)
}func TestCompareArray(t *testing.T) {a := [...]int{1, 2, 3, 4}b := [...]int{1, 3, 2, 4}// c := [...]int{1, 2, 3, 4, 5}d := [...]int{1, 2, 3, 4}t.Log(a == b)t.Log(a == d)
}func TestBitClear(t *testing.T) {a := 7 // 0111 (所有標志位都設置)a = a &^ Readable // 清除Readable位a = a &^ Executable // 清除Executable位t.Log(a&Readable == Readable, a&Writeable == Writeable, a&Executable == Executable)
}
//&^位清除
//輸出結果:Final value: 2 (0010)
輸出結果:
func TestOperatorBasic(t *testing.T) {
Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestOperatorBasic$ go_code/chapter/operator=== RUN TestOperatorBasic
Sum: 15
Difference: 5
Product: 50
Quotient: 2
Remainder 0
AND: false
OR: true
NOT: false
Equal: false
Not Equal: true
Greater Than: true
Less Than: false
Greater Than or Equal: true
Less Than or Equal: false
--- PASS: TestOperatorBasic (0.00s)
PASS
ok go_code/chapter/operator (cached)
func TestCompareArray(t *testing.T) {
Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestCompareArray$ go_code/chapter/operator=== RUN TestCompareArrayd:\Microsoft VS Code\GOproject\src\go_code\chapter\operator\operator_test.go:45: falsed:\Microsoft VS Code\GOproject\src\go_code\chapter\operator\operator_test.go:46: true
--- PASS: TestCompareArray (0.00s)
PASS
ok go_code/chapter/operator (cached)
func TestBitClear(t *testing.T) {
Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestBitClear$ go_code/chapter/operator=== RUN TestBitCleard:\Microsoft VS Code\GOproject\src\go_code\chapter\operator\operator_test.go:53: false true false
--- PASS: TestBitClear (0.00s)
PASS
ok go_code/chapter/operator (cached)
const (...)
: 定義了三個常量 Readable
、Writable
和 Executable
,使用位移操作生成不同的值。
func TestOperatorBasic(t *testing.T) { ... }
: 定義了一個測試函數 “TestOperatorBasic”,用于測試基本運算符的使用。
算術運算符:展示了加法、減法、乘法、除法和取余運算;邏輯運算符:展示了邏輯與、邏輯或和邏輯非運算;比較運算符:展示了等于、不等于、大于、小于、大于等于和小于等于運算。
func TestCompareArray(t *testing.T) { ... }
: 定義了一個測試函數 “TestCompareArray”,用于測試數組的比較。
聲明了兩個整數數組 a
和 b
,以及另一個數組 d
,其中數組 a
和數組 d
的內容相同;使用比較運算符 ==
檢查數組 a
和 b
是否相等,以及數組 a
和 d
是否相等。
func TestBitClear(t *testing.T) { ... }
: 定義了一個測試函數 “TestBitClear”,用于測試位清除操作。
聲明一個整數變量 a
,并將其初始化為 7
,即二進制表示 0111;
使用位清除操作 &^
將 a
中的 Readable
和 Executable
位清除;使用按位與運算 &
檢查 a
是否具有 Readable
、Writable
和 Executable
屬性。
**按位清除運算符 &^ **
在Go語言中,&^
是按位清除運算符(Bit Clear Operator)。它用于將某些位置上的位清零,即將指定位置上的位設置為0。&^
運算符在處理二進制位操作時非常有用。
&^
運算符執行以下操作:
對于每個位,如果右側操作數的對應位為 0,則結果位與左側操作數相同。
對于每個位,如果右側操作數的對應位為 1,則結果位被強制設置為 0。
這意味著,&^
運算符用于“清除”左側操作數的特定位,使其與右側操作數的相應位不受影響。寫個代碼驗證下:
package mainimport "fmt"func main() {a := 0b11001100 //204b := 0b10101010 //170result := a &^ b //68fmt.Println("示例:", a, b, result)
}
計算過程:
- 第1位(最高位):a=1, b=1 → 0
- 第2位:a=1, b=0 → 1
- 第3位:a=0, b=1 → 0
- 第4位:a=0, b=0 → 0
- 第5位:a=1, b=1 → 0
- 第6位:a=1, b=0 → 1
- 第7位:a=0, b=1 → 0
- 第8位(最低位):a=0, b=0 → 0
輸出結果:
PS D:\Microsoft VS Code\GOproject\src\go_code> go run .\go.go
示例: 204 170 68
條件語句
if語句
if
語句用于基于條件來決定是否執行某段代碼。它的基本語法如下:
if condition {// 代碼塊
} else if anotherCondition {// 代碼塊
} else {// 代碼塊
}
switch 語句
switch
語句用于基于表達式的不同值執行不同的代碼分支。****與其他語言不同,Go的switch
可以自動匹配第一個滿足條件的分支,而無需使用break
語句。****它的語法如下:
switch expression {
case value1:// 代碼塊
case value2:// 代碼塊
default:// 代碼塊
}
測試代碼:
package conditionimport ("fmt""testing"
)func TestConditionIf(t *testing.T) {age := 18if age < 18 {fmt.Println("you are a boy")} else if age >= 18 && age <= 60 {fmt.Println("you are an adult")} else {fmt.Println("ypu are a senior citizen")}
}func TestConditionSwitch(t *testing.T) {dayOfWeek := 7switch dayOfWeek {case 1:fmt.Println("Monday")case 2:fmt.Println("Tuseday")case 3:fmt.Println("Wednesday")case 4:fmt.Println("Thursday")case 5:fmt.Println("Friday")default:fmt.Println("Weekend!")}
}func TestSwitchCaseCondition(t *testing.T) {for i := 0; i < 5; i++ {switch {case i%2 == 0:t.Logf("%d is Even", i) //偶數case i%2 == 1:t.Logf("%d is Odd", i) //奇數default:t.Logf("%d is unknow", i)}}
}
下面逐個解釋每個測試函數的內容:
func TestConditionIf(t *testing.T) { ... }
:測試 if
語句的使用。根據年齡的不同情況,通過 if
、else if
和 else
分支判斷是否為未成年人、成年人或老年人。
func TestConditionSwitch(t *testing.T) { ... }
:測試 switch
語句的使用。根據 dayOfWeek
的值,使用 switch
語句輸出對應的星期幾。
func TestSwitchMultiCase(t *testing.T) { ... }
:測試 switch
語句多個 case
值的情況。使用 switch
語句判斷每個數字的奇偶性,并輸出相應的信息。
func TestSwitchCaseCondition(t *testing.T) { ... }
:測試 switch
語句中的條件表達式。使用 switch
語句通過對數字取余判斷數字的奇偶性,并輸出相應的信息。
這些測試函數展示了Go語言中條件語句的不同用法,包括基于條件的分支判斷和多個 case
值的處理,以及在 switch
語句中使用條件表達式的情況。
輸出結果:
Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestConditionIf$ go_code/chapter/condition=== RUN TestConditionIf
you are an adult
--- PASS: TestConditionIf (0.00s)
PASS
ok go_code/chapter/condition 0.059sRunning tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestConditionSwitch$ go_code/chapter/condition=== RUN TestConditionSwitch
Weekend!
--- PASS: TestConditionSwitch (0.00s)
PASS
ok go_code/chapter/condition 0.057sRunning tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestSwitchCaseCondition$ go_code/chapter/condition=== RUN TestSwitchCaseConditiond:\Microsoft VS Code\GOproject\src\go_code\chapter\condition\condition_test.go:43: 0 is Evend:\Microsoft VS Code\GOproject\src\go_code\chapter\condition\condition_test.go:45: 1 is Oddd:\Microsoft VS Code\GOproject\src\go_code\chapter\condition\condition_test.go:43: 2 is Evend:\Microsoft VS Code\GOproject\src\go_code\chapter\condition\condition_test.go:45: 3 is Oddd:\Microsoft VS Code\GOproject\src\go_code\chapter\condition\condition_test.go:43: 4 is Even
--- PASS: TestSwitchCaseCondition (0.00s)
PASS
ok go_code/chapter/condition (cached)
## 循環語句
for
循環
for
循環用于重復執行代碼塊,支持初始化語句、循環條件和循環后的語句。它的基本形式如下:
for initialization; condition; post {// 代碼塊
}
在初始化語句中,您可以初始化循環變量,然后在循環體中使用條件來控制循環,最后在 post
語句中執行遞增或遞減操作。
for
循環的簡化形式
Go語言的 for
循環還可以簡化成只有循環條件部分,類似于其他語言中的 while
循環:
for condition {// 代碼塊
}
range
循環
range
循環用于迭代數組、切片、映射、字符串等可迭代的數據結構。它返回每次迭代的索引和值。示例:
for index, value := range iterable {// 使用 index 和 value
}
創建loop_test.go進行驗證分析, 具體代碼如下:
package loopimport ("fmt""testing"
)func TestLoopFor(t *testing.T) {for i := 1; i <= 5; i++ {fmt.Println("Iteration:", i)}
}func TestLoopForBasic(t *testing.T) {i := 1for i <= 5 {fmt.Println("Iteration:", i)i++}
}func TestLoopForRange(t *testing.T) {numbers := []int{1, 2, 3, 4, 5}for index, value := range numbers {fmt.Printf("Index: %d, Value: %d\n", index, value)}
}func TestLoopForUnLimit(t *testing.T) {i := 1for {fmt.Println("Iteration:", i)i++if i > 5 {break}}
}
下面逐個解釋每個測試函數的內容:
func TestLoopFor(t *testing.T) { ... }
:測試基本的 for
循環。使用 for
循環,從 1 到 5 迭代輸出循環迭代次數。
func TestLoopForBasic(t *testing.T) { ... }
:測試不帶初始化語句的 for
循環。使用 for
循環,從 1 到 5 迭代輸出循環迭代次數,但沒有在循環頭部聲明初始化語句。
func TestLoopForRange(t *testing.T) { ... }
:測試使用 for range
迭代切片。定義一個整數切片 numbers
,使用 for range
循環迭代切片中的每個元素,輸出元素的索引和值。
func TestLoopForUnLimit(t *testing.T) { ... }
:測試無限循環及 break
語句。使用無限循環和 break
語句,在循環體內部判斷是否終止循環,當 i
大于 5 時退出循環。
這些測試函數展示了Go語言中不同類型的 for
循環的用法,包括標準的計數循環、不帶初始化語句的循環、遍歷切片以及無限循環與循環終止條件。
測試輸出:
Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestLoopFor$ go_code/chapter/loop=== RUN TestLoopFor
Iteration: 1
Iteration: 2
Iteration: 3
Iteration: 4
Iteration: 5
--- PASS: TestLoopFor (0.00s)
PASS
ok go_code/chapter/loop 0.073sRunning tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestLoopForBasic$ go_code/chapter/loop=== RUN TestLoopForBasic
Iteration: 1
Iteration: 2
Iteration: 3
Iteration: 4
Iteration: 5
--- PASS: TestLoopForBasic (0.00s)
PASS
ok go_code/chapter/loop 0.060sRunning tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestLoopForRange$ go_code/chapter/loop=== RUN TestLoopForRange
Index: 0, Value: 1
Index: 1, Value: 2
Index: 2, Value: 3
Index: 3, Value: 4
Index: 4, Value: 5
--- PASS: TestLoopForRange (0.00s)
PASS
ok go_code/chapter/loop 0.063sRunning tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestLoopForUnLimit$ go_code/chapter/loop=== RUN TestLoopForUnLimit
Iteration: 1
Iteration: 2
Iteration: 3
Iteration: 4
Iteration: 5
--- PASS: TestLoopForUnLimit (0.00s)
PASS
ok go_code/chapter/loop (cached)
跳轉語句
Go語言也支持幾種跳轉語句,用于在循環和條件中控制流程:
break
:跳出循環。continue
:跳過本次循環迭代,繼續下一次迭代。goto
:在代碼中直接跳轉到指定標簽處**(不推薦使用)**。
創建jump_test.go進行驗證分析, 具體代碼如下:
package jumpimport ("fmt""testing"
)func TestJumpBreak(t *testing.T) {for i := 1; i <= 5; i++ {if i == 3 {break}fmt.Println("Iteration:", i)}
}func TestJumpContinue(t *testing.T) {for i := 1; i <= 5; i++ {if i == 3 {continue}fmt.Println("Iteration:", i)}
}func TestJumpGoto(t *testing.T) {i := 1start:fmt.Println("Iteration:", i)i++if i <= 5 {goto start}
}
下面逐個解釋每個測試函數的內容:
func TestJumpBreak(t *testing.T) { ... }
:測試 break
語句的使用。使用 for
循環迭代從 1 到 5,但當迭代變量 i
等于 3 時,使用 break
語句終止循環。
func TestJumpContinue(t *testing.T) { ... }
:測試 continue
語句的使用。使用 for
循環迭代從 1 到 5,但當迭代變量 i
等于 3 時,使用 continue
語句跳過該次迭代繼續下一次迭代。
func TestJumpGoto(t *testing.T) { ... }
:測試 goto
語句的使用。使用 goto
語句實現了一個無限循環,即使用標簽 start
和 goto start
在循環體內部跳轉到循環的起始位置。循環的終止條件是當 i
大于 5 時。
這些測試函數展示了Go語言中的循環控制跳轉語句,包括用于終止循環的 break
、用于跳過當前迭代的 continue
,以及用于無限循環的 goto
語句。
測試結果一目了然:
Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestJumpBreak$ go_code/chapter/jump=== RUN TestJumpBreak
Iteration: 1
Iteration: 2
--- PASS: TestJumpBreak (0.00s)
PASS
ok go_code/chapter/jump 0.061sRunning tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestJumpContinue$ go_code/chapter/jump=== RUN TestJumpContinue
Iteration: 1
Iteration: 2
Iteration: 4
Iteration: 5
--- PASS: TestJumpContinue (0.00s)
PASS
ok go_code/chapter/jump 0.062sRunning tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestJumpGoto$ go_code/chapter/jump=== RUN TestJumpGoto
Iteration: 1
Iteration: 2
Iteration: 3
Iteration: 4
Iteration: 5
--- PASS: TestJumpGoto (0.00s)
PASS
ok go_code/chapter/jump (cached)
常用集合和字符串
數組
Go語言中的數組是一種固定長度、同類型元素的集合。
特點:
- 數組的長度在聲明時指定,且在創建后不可更改。
- 數組是值類型,當數組被賦值給新變量或作為參數傳遞時,會創建一個新的副本。
- 數組在內存中是連續存儲的,支持隨機訪問。
數組聲明和初始化:格式為
var arrayName [size]dataType
arrayName
:數組的名稱。size
:數組的長度,必須是一個常量表達式。dataType
:數組存儲的元素類型。
數組初始化方式:
// 使用指定的值初始化數組
var arr = [5]int{1, 2, 3, 4, 5}// 根據索引初始化數組
var arr [5]int
arr[0] = 10
arr[1] = 20// 部分初始化
var arr = [5]int{1, 2}// 自動推斷數組長度
arr := [...]int{1, 2, 3, 4, 5}
數組的訪問和遍歷:
// 訪問單個元素
value := arr[index]// 遍歷數組
for index, value := range arr {fmt.Printf("Index: %d, Value: %d\n", index, value)
}
//輸出結果類似于:
Index: 0, Value: 1
Index: 1, Value: 2
Index: 2, Value: 3
Index: 3, Value: 4
Index: 4, Value: 5
數組作為函數:數組在函數參數傳遞時會創建副本,因此對函數內的數組修改不會影響原始數組。如果需要在函數內修改原始數組,可以傳遞指向數組的指針。
func modifyArray(arr [5]int) {arr[0] = 100
}func modifyArrayByPointer(arr *[5]int) {arr[0] = 100
}
多維數組
Go語言支持多維數組,例如二維數組和三維數組。多維數組的初始化和訪問與一維數組類似,只需要指定多個索引。
var matrix [3][3]int = [3][3]int{{1, 2, 3},{4, 5, 6},{7, 8, 9},
}
**數組在存儲固定數量的同類型元素時非常有用,但由于其固定長度的限制,通常在實際開發中更常用的是切片,它具有動態長度的特性。**切片可以根據需要進行增加、刪除和重新分配,更加靈活。
測試代碼:
package arrayimport "testing"func TestArrayInit(t *testing.T) {var arr [3]intarr1 := [4]int{1, 2, 3, 4} //[1 2 3 4]arr3 := [...]int{1, 3, 4, 5} //[1 3 4 5]arr1[1] = 5 //修改數組arr1的第2個元素(索引從0開始)t.Log(arr[1], arr[2]) //所有元素會被自動初始化為int類型的零值(即0)t.Log(arr1, arr3)
}//輸出結果
// 0 0
//[1 5 3 4] [1 3 4 5]func TestArrayTravel(t *testing.T) {arr3 := [...]int{1, 3, 4, 5}for i := 0; i < len(arr3); i++ {t.Log(arr3[i])}for _, e := range arr3 {t.Log(e)}
}func TestArraySection(t *testing.T) {arr3 := [...]int{1, 2, 3, 4, 5}arr3_sec := arr3[:]t.Log(arr3_sec)
}
下面逐個解釋每個測試函數的內容:
func TestArrayInit(t *testing.T) { ... }
:測試數組的初始化。
使用不同的方式初始化數組 arr
,arr1
和 arr3;
修改 arr1
的第二個元素為 5;
使用 t.Log()
輸出不同數組的元素值和內容。
func TestArrayTravel(t *testing.T) { ... }
:測試數組的遍歷。
使用 for
循環遍歷數組 arr3
,分別輸出每個元素的值;使用 for range
循環遍歷數組 arr3
,同樣輸出每個元素的值。
func TestArraySection(t *testing.T) { ... }
:測試數組切片的使用。
創建一個數組切片 arr3_sec
,基于整個數組 arr3;
使用 t.Log()
輸出數組切片 arr3_sec
的內容。
測試輸出:
Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestArrayInit$ go_code/chapter/array1=== RUN TestArrayInitd:\Microsoft VS Code\GOproject\src\go_code\chapter\array1\array_test.go:10: 0 0d:\Microsoft VS Code\GOproject\src\go_code\chapter\array1\array_test.go:11: [1 5 3 4] [1 3 4 5]
--- PASS: TestArrayInit (0.00s)
PASS
ok go_code/chapter/array1 0.074sRunning tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestArrayTravel$ go_code/chapter/array1=== RUN TestArrayTraveld:\Microsoft VS Code\GOproject\src\go_code\chapter\array1\array_test.go:22: 1d:\Microsoft VS Code\GOproject\src\go_code\chapter\array1\array_test.go:22: 3d:\Microsoft VS Code\GOproject\src\go_code\chapter\array1\array_test.go:22: 4d:\Microsoft VS Code\GOproject\src\go_code\chapter\array1\array_test.go:22: 5d:\Microsoft VS Code\GOproject\src\go_code\chapter\array1\array_test.go:25: 1d:\Microsoft VS Code\GOproject\src\go_code\chapter\array1\array_test.go:25: 3d:\Microsoft VS Code\GOproject\src\go_code\chapter\array1\array_test.go:25: 4d:\Microsoft VS Code\GOproject\src\go_code\chapter\array1\array_test.go:25: 5
--- PASS: TestArrayTravel (0.00s)
PASS
ok go_code/chapter/array1 0.064sRunning tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestArraySection$ go_code/chapter/array1=== RUN TestArraySectiond:\Microsoft VS Code\GOproject\src\go_code\chapter\array1\array_test.go:33: [1 2 3 4 5]
--- PASS: TestArraySection (0.00s)
PASS
ok go_code/chapter/array1 (cached)
切片
Go語言中的切片(Slice)是對數組的一層封裝,提供了更靈活的動態長度序列。
切片的特點
- 切片是引用類型,它不保存數據,只是引用底層數組的一部分。
- 切片是動態長度的,可以根據需要進行擴容或縮減。
- 切片是可索引的,并且可以通過切片索引進行切割。
切片的聲明和初始化
var sliceName []elementType
切片的初始化方式
// 聲明切片并初始化
var slice = []int{1, 2, 3, 4, 5}// 使用 make 函數創建切片
var slice = make([]int, 5) // 創建長度為 5 的 int 類型切片// 使用切片切割已有數組或切片
newSlice := oldSlice[startIndex:endIndex] // 包括 startIndex,但不包括 endIndex
切片的內置函數和操作
len(slice)
:返回切片的長度。cap(slice)
:返回切片的容量,即底層數組的長度。append(slice, element)
:將元素追加到切片末尾,并返回新的切片。copy(destination, source)
:將源切片中的元素復制到目標切片。
切片的遍歷
for index, value := range slice {// 使用 index 和 value
}
切片作為函數參數
切片作為參數傳遞給函數時,函數內部對切片的修改會影響到原始切片。
func modifySlice(s []int) {s[0] = 100
}func main() {numbers := []int{1, 2, 3, 4, 5}modifySlice(numbers)fmt.Println(numbers) // 輸出:[100 2 3 4 5]
}
切片在Go語言中廣泛用于處理動態數據集,例如集合、列表、隊列等。它提供了方便的方法來管理元素,同時避免了固定數組的限制。在實際應用中,切片經常被用于存儲和處理變長數據。
創建slice_test.go進行驗證分析, 具體代碼如下:
package sliceimport ("fmt""testing"
)func TestSlice(t *testing.T) {//聲明和初始化切片numbers := []int{1, 2, 3, 4, 5}fmt.Println("Original Slice:", numbers)//輸出:Original Slice: [1 2 3 4 5]//使用make函數創建切片slice := make([]int, 3)fmt.Println("Initial Make Slice:", slice)//輸出:Initial Make Slice: [0 0 0]//添加元素到切片slice = append(slice, 10)slice = append(slice, 20, 30)fmt.Println("After Append:", slice)//輸出:After Append: [0 0 0 10 20 30]//復制切片copySlice := make([]int, len(slice))copy(copySlice, slice)fmt.Println("Copied Slic:e:", copySlice)//輸出:Copied Slice: [0 0 0 10 20 30]//切片切割subSlice := numbers[1:3]fmt.Println("Subslice:", subSlice)//輸出:Subslice: [2 3]//修改切片值會影響底層數組和其他切片subSlice[0] = 100fmt.Println("Modified Subslice:", subSlice)fmt.Println("Orginal Slice:", numbers)fmt.Println("Copied Slice:", copySlice)//Copied Slice: [0 0 0 10 20 30]//遍歷切片for index, value := range slice {fmt.Printf("Index:%d,Value:%d\n", index, value)}
}
下面逐個解釋每個測試函數的內容:
func TestSlice(t *testing.T) { ... }
:測試切片的基本操作。
- 聲明和初始化切片
numbers
,輸出初始切片內容。 - 使用
make
函數創建初始容量為3
的切片slice
,輸出初始切片內容。 - 使用
append
函數向切片slice
添加元素。 - 使用
copy
函數復制切片slice
到新的切片copySlice
。 - 使用切片
numbers
進行切片切割,創建子切片subSlice
。 - 修改
subSlice
的第一個元素為100
,輸出修改后的切片和原始切片,以及復制的切片。 - 使用
for range
循環遍歷切片slice
,輸出每個元素的索引和值。
這個測試函數展示了Go語言中切片的各種操作,包括切片的創建、添加元素、復制切片、切片切割、修改切片元素等。
Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestSlice$ go_code/chapter/slice=== RUN TestSlice
Original Slice: [1 2 3 4 5]
Initial Make Slice: [0 0 0]
After Append: [0 0 0 10 20 30]
Copied Slic:e: [0 0 0 10 20 30]
Subslice: [2 3]
Modified Subslice: [100 3]
Orginal Slice: [1 100 3 4 5]
Copied Slice: [0 0 0 10 20 30]
Index:0,Value:0
Index:1,Value:0
Index:2,Value:0
Index:3,Value:10
Index:4,Value:20
Index:5,Value:30
--- PASS: TestSlice (0.00s)
PASS
ok go_code/chapter/slice 0.405s
Map
Go語言中的映射(Map)是鍵值對的無序集合,也被稱為關聯數組或字典。
映射的特點
- 映射用于存儲一組鍵值對,其中每個鍵都是唯一的。
- 映射是無序的,無法保證鍵值對的順序。
- 鍵可以是任何可比較的類型,值可以是任意類型。
- 映射是引用類型,可以被賦值和傳遞給函數。
映射的聲明和初始化
var mapName map[keyType]valueType
映射的初始化方式
// 聲明和初始化映射
var ages = map[string]int{"Alice": 25,"Bob": 30,"Eve": 28,
}// 使用 make 函數創建映射
var ages = make(map[string]int)
映射的操作
- 添加鍵值對:
ages["Charlie"] = 35
- 刪除鍵值對:
delete(ages, "Eve")
- 獲取值:
value := ages["Alice"]
映射的遍歷
for key, value := range ages {fmt.Printf("Name: %s, Age: %d\n", key, value)
}
映射作為函數參數
映射作為參數傳遞給函數時,函數內部對映射的修改會影響到原始映射。
func modifyMap(m map[string]int) {m["Alice"] = 30
}func main() {ages := map[string]int{"Alice": 25,"Bob": 30,}modifyMap(ages)fmt.Println(ages) // 輸出:map[Alice:30 Bob:30]
}
映射在Go語言中用于存儲和檢索數據,是一種非常常用的數據結構。它在存儲一組關聯的鍵值對時非常有用,比如存儲姓名與年齡的對應關系、單詞與定義的對應關系等。在實際應用中,映射是處理和存儲鍵值數據的重要工具。
創建map_test.go進行驗證分析, 具體代碼如下:
package my_mapimport ("fmt""testing"
)func TestBasic(t *testing.T) {//聲明和初始化映射ages := map[string]int{"hzy": 18,"thm": 19,"txd": 30,}fmt.Println("Original Map:", ages)//添加新的鍵值對ages["Charlie"] = 20fmt.Println("After Adding:", ages)//修改已有的鍵值對ages["hzy"] = 21fmt.Println("After Modification:", ages)//刪除鍵值對delete(ages, "thm")fmt.Println("After Delection:", ages)//獲取值和檢查校驗值是否存在age, exists := ages["txd"]if exists {fmt.Println("txd ages is :", age)} else {fmt.Println("txd not found")}//遍歷映射for name, age := range ages {fmt.Printf("Name:%sm,Age:%d\n", name, age)}
}type Student struct {Name stringAge intGrade string
}//定義結構體,三個字段,結構體用于組織相關聯的數據func TestComplex(t *testing.T) {// 聲明和初始化映射,用于存儲學生信息和成績studentScores := make(map[string]int)studentInfo := make(map[string]Student)// 添加學生信息和成績studentInfo["Alice"] = Student{Name: "Alice", Age: 18, Grade: "A"}studentScores["Alice"] = 80// 查找學生信息和成績aliceInfo := studentInfo["Alice"]aliceScore := studentScores["Alice"]fmt.Printf("Name: %s, Age: %d, Grade: %s, Score: %d\n", aliceInfo.Name, aliceInfo.Age, aliceInfo.Grade, aliceScore)// 遍歷學生信息和成績for name, info := range studentInfo {score, exits := studentScores[name]if exits {fmt.Printf("Name:%s,Age:%d,Grade:%s,Score:%d\n", info.Name, info.Age, info.Grade, score)} else {fmt.Printf("No score available for %s\n", name)}}
}
下面逐個解釋每個測試函數的內容:
func TestBasic(t *testing.T) { ... }
:測試映射的基本操作。
聲明和初始化映射 ages
,存儲人名和年齡的鍵值對;輸出初始映射內容;使用 ages["Charlie"]
添加新的鍵值對;使用 ages["Bob"]
修改已有鍵的值;使用 delete
函數刪除鍵值對;使用 age, exists
來獲取值并檢查鍵是否存在;使用 for range
循環遍歷映射,輸出每個鍵值對的信息。
type Student struct { ... }
:定義了一個名為 Student
的結構體,用于存儲學生信息。
func TestComplex(t *testing.T) { ... }
:測試包含復雜值的映射操作。
聲明和初始化兩個映射,studentScores
用于存儲學生分數,studentInfo
用于存儲學生信息;添加學生信息和分數到映射;使用 studentInfo["Alice"]
獲取學生信息,使用 studentScores["Alice"]
獲取學生分數;使用 for range
循環遍歷映射,輸出每個學生的信息和分數。
這些測試函數展示了Go語言中映射的各種操作,包括創建、添加、修改、刪除鍵值對,檢查鍵是否存在,以及遍歷映射的鍵值對。
輸出結果:
Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestBasic$ go_code/chapter/map=== RUN TestBasic
Original Map: map[hzy:18 thm:19 txd:30]
After Adding: map[Charlie:20 hzy:18 thm:19 txd:30]
After Modification: map[Charlie:20 hzy:21 thm:19 txd:30]
After Delection: map[Charlie:20 hzy:21 txd:30]
txd ages is : 30
Name:hzym,Age:21
Name:txdm,Age:30
Name:Charliem,Age:20
--- PASS: TestBasic (0.00s)
PASS
ok go_code/chapter/map 0.425sRunning tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestComplex$ go_code/chapter/map=== RUN TestComplex
Name: Alice, Age: 18, Grade: A, Score: 80
Name:Alice,Age:18,Grade:A,Score:80
--- PASS: TestComplex (0.00s)
PASS
ok go_code/chapter/map 0.073s
實現Set
在Go語言中,雖然標準庫沒有提供內置的Set類型,但你可以使用多種方式來實現Set的功能。以下是幾種常見的實現Set的方式介紹:
使用切片
創建set_slice_test.go練習
使用切片來存儲元素,通過遍歷切片來檢查元素是否存在。這是一個簡單的實現方式,適用于小型的集合。
package setimport ("fmt""testing"
)type InSet struct {elements []int
}func (s *InSet) Add(element int) {if !s.Contains(element) {s.elements = append(s.elements, element)}
}// 先檢查元素是否已存在
// 不存在時才添加到切片
// 使用指針接收者(*IntSet)因為要修改結構體func (s *InSet) Contains(element int) bool {for _, e := range s.elements {if e == element {return true}}return false
}//線性搜索切片中的元素;找到返回true,否則返回falsefunc TestSet(t *testing.T) {set := InSet{}set.Add(1)set.Add(2)set.Add(3)set.Add(2) // 這個重復添加會被忽略fmt.Println("Set:", set.elements)
}
測試結果:
Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestSet$ go_code/chapter/set_slice=== RUN TestSet
Set: [1 2 3]
--- PASS: TestSet (0.00s)
PASS
ok go_code/chapter/set_slice 0.407s
使用映射
創建set_map_test.go練習
使用映射來存儲元素,映射的鍵代表集合的元素,值可以是任意類型。這樣的實現方式更快速,適用于大型的集合,因為映射的查找復雜度為 O(1)。
package setimport ("fmt""testing"
)type Set map[int]boolfunc (s Set) Add(element int) {s[element] = true
}func (s Set) Contains(element int) bool {return s[element]
}func TestSetMap(t *testing.T) {set := make(Set)set.Add(1)set.Add(2)set.Add(3)fmt.Println("Set:", set)
}
測試輸出:
Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestSetMap$ go_code/chapter/set_slice=== RUN TestSetMap
Set: map[1:true 2:true 3:true]
--- PASS: TestSetMap (0.00s)
PASS
ok go_code/chapter/set_slice 0.162s
使用第三方庫
創建set_third_test.go練習
為了避免自行實現,你可以使用一些第三方庫,例如 github.com/deckarep/golang-set
,它提供了更豐富的Set功能。
添加個代理:go env -w GOPROXY=https://goproxy.io,direct
然后安裝包:go get github.com/deckarep/golang-set
PS D:\Microsoft VS Code\GOproject\src\go_code\chapter> go get github.com/deckarep/golang-set
go: downloading github.com/deckarep/golang-set v1.8.0
go: added github.com/deckarep/golang-set v1.8.0
PS D:\Microsoft VS Code\GOproject\src\go_code\chapter>
set_third_test.go:
package setimport ("fmt""testing"mapset "github.com/deckarep/golang-set"
)func TestSetThird(t *testing.T) {inSet := mapset.NewSet()inSet.Add(1)inSet.Add(2)inSet.Add(3)fmt.Println("Set:", inSet)
}
測試結果:
Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestSetThird$ go_code/chapter=== RUN TestSetThird
Set: Set{1, 2, 3}
--- PASS: TestSetThird (0.00s)
PASS
ok go_code/chapter 0.177s
字符串
字符串的聲明與初始化
在Go語言中,字符串是由一系列字符組成的,可以使用雙引號 "
或反引號 ```來聲明和初始化字符串
package mainimport "fmt"func main() {str1 := "Hello World!" //雙引號str2 := `Hello Go!` //反引號fmt.Println(str1)fmt.Println(str2)
}
輸出:
[Running] go run "d:\Microsoft VS Code\GOproject\src\go_code\chapter\string\string.go"
Hello World!
Hello Go![Done] exited with code=0 in 1.392 seconds
字符串的長度
使用內置函數 len()
可以獲取字符串的長度,即字符串中字符的個數。
package mainimport "fmt"func main() {str1 := "Hello World"length := len(str1)fmt.Println("String Length is ", length)
}
輸出:
[Running] go run "d:\Microsoft VS Code\GOproject\src\go_code\chapter\string\string.go"
String Length is 11[Done] exited with code=0 in 1.404 seconds
字符串的索引與切片
字符串中的字符可以通過索引訪問,索引從0開始。可以使用切片操作來獲取字符串的子串。
package mainimport "fmt"func main() {Str := "Hello,World"//獲取第一個字符firstChar := Str[0]fmt.Println("First Character:", string(firstChar))//獲取子串substring := Str[6:11]fmt.Println("Substring:", substring)
}
輸出:
[Running] go run "d:\Microsoft VS Code\GOproject\src\go_code\chapter\string\string.go"
First Character: H
Substring: World[Done] exited with code=0 in 1.552 seconds
字符串拼接
使用 +
運算符可以將兩個字符串連接成一個新的字符串。
strings.Join
函數用于將字符串切片連接成一個新的字符串,可以用來拼接多個字符串。
最后,使用字節緩沖可以在不產生多余字符串副本的情況下進行高效的字符串拼接。
package mainimport ("bytes""fmt""strings"
)func main() {str1 := "Hello,"str2 := "World!"result1 := str1 + str2fmt.Println("Concatenated String:", result1)//拼接字符串切片(slice)strSlice := []string{"Hello", "", "World!"}result2 := strings.Join(strSlice, "")fmt.Println(result2)//使用緩沖區,避免頻繁內存分配var buffer bytes.Bufferbuffer.WriteString(str1)buffer.WriteString(str2)result3 := buffer.String()fmt.Println(result3)
}//總結:
// 少量簡單拼接:使用 + 運算符
// 已有字符串切片:使用 strings.Join
// 大量或循環拼接:使用 bytes.Buffer(或 Go 1.10+ 的 strings.Builder)
輸出結果:
[Running] go run "d:\Microsoft VS Code\GOproject\src\go_code\chapter\string\string.go"
Concatenated String: Hello,World!
HelloWorld!
Hello,World![Done] exited with code=0 in 1.435 seconds
多行字符串
使用反引號 ``` 來創建多行字符串。
package mainimport "fmt"func main() {multiLineStr := `this is a multi-linestring. `fmt.Println(multiLineStr)
}
輸出結果:
[Running] go run "d:\Microsoft VS Code\GOproject\src\go_code\chapter\string\string.go"this is a multi-linestring. [Done] exited with code=0 in 1.401 seconds
字符串迭代
使用 for range
循環迭代字符串的每個字符。
package mainimport "fmt"func main() {str := "Go 語言"for _, char := range str {fmt.Printf("%c", char)}
}
輸出結果:
[Running] go run "d:\Microsoft VS Code\GOproject\src\go_code\chapter\string\string.go"
Go 語言
[Done] exited with code=0 in 1.366 seconds
字符串和字節數組之間的轉換
在Go語言中,字符串和字節數組之間可以進行相互轉換。
package mainimport "fmt"func main() {str := "Hello"bytes := []byte(str) // 字符串轉字節切片//Go 的字符串本質是只讀的字節切片([]byte),存儲的是字符串的UTF-8 編碼字節序列// 每個字節對應字符的 ASCII 碼:// H → 72// e → 101// l → 108// l → 108// o → 111strAgain := string(bytes)//string(bytes) 會基于字節切片創建一個新的字符串,復制字節數據。//轉換后的字符串仍然是UTF-8 編碼fmt.Println("Bytes:", bytes)fmt.Println("String Again:", strAgain)
}
輸出結果:
[Running] go run "d:\Microsoft VS Code\GOproject\src\go_code\chapter\string\string.go"
Bytes: [72 101 108 108 111]
String Again: Hello[Done] exited with code=0 in 2.307 seconds
字符串比較
字符串的比較可以使用 ==
和 !=
運算符。當然還有其他函數類型的直接應用的:strings.Compare
函數用于比較兩個字符串,并根據比較結果返回一個整數。
也可以使用自定義的比較函數來比較字符串,根據自己的需求定義比較邏輯。
package mainimport ("fmt""strings"
)func customCompare(str1, str2 string) bool {return str1 == str2
}func main() {str1 := "Hello"str2 := "World"if str1 == str2 {fmt.Println("String are equal")} else {fmt.Println("String are not equal")}// == 是 Go 語言的基本比較運算符// 它會逐個字節比較兩個字符串的內容// 比較是基于字符串的底層字節表示(UTF-8 編碼)result := strings.Compare(str1, str2)if result == 0 {fmt.Println("String are equal")} else if result < 0 {fmt.Println("str1 is less than str2")} else {fmt.Println("str1 is greater than str2")}//返回一個整數:// 0 表示 a == b// -1 表示 a < b// 1 表示 a > bif customCompare(str1, str2) {fmt.Println("Strings are equal")} else {fmt.Println("String are not equal")}// 這實際上是 == 運算符的包裝函數// 功能與直接使用 == 完全相同
}
輸出結果:
[Running] go run "d:\Microsoft VS Code\GOproject\src\go_code\chapter\string\string.go"
String are not equal
str1 is less than str2
String are not equal[Done] exited with code=0 in 1.22 seconds
這些基本概念和操作可以幫助你更好地理解和使用Go語言中的字符串。要注意字符串的不可變性,以及與其他數據類型的轉換和比較。
創建string_test.go練習:
package stringimport ("strconv""strings""testing"
)func TestString(t *testing.T) {var s stringt.Log(s) //初始化為默認零值“”空字符串長度為 0,但會占用固定的內存空間s = "Hello"t.Log(len(s))//len(s) 返回字符串的??字節長度??,不是字符數//s[1] = '3'//string是不可變的byte slice//s = "\xE4\xB8\xA5" //可以存儲任何二進制數據,亂碼s = "\xE4\xBA\xBB\xFF"// Go 字符串可以存儲??任意二進制數據??,不只是 UTF-8 文本// \x 表示十六進制字節值,這里存儲了 4 個字節// 打印時如果字節序列不是有效的 UTF-8,可能顯示為亂碼// 長度計算的是字節數,不是字符數t.Log(s)t.Log(len(s))s = "中"//中文字符"中"在 UTF-8 中占 3 字節t.Log(len(s))c := []rune(s)//將字符串轉換為 Unicode 碼點序列t.Log(len(c))// 輸出: 1 (1個Unicode字符)t.Logf("中 unicode %x", c[0])t.Logf("中 UTF8 %x", s)
}// 字符串與 rune 轉換
func TestStringToRune(t *testing.T) {s := "中華人民共和國"for _, c := range s {t.Logf("%[1]c %[1]x", c)}//每次迭代返回的是 Unicode 碼點(rune 類型),而不是字節// %[1]c:以字符形式輸出 rune// %[1]x:以十六進制形式輸出 rune 的 Unicode 碼點// [1] 表示使用第一個參數(即 c)
}// 字符串分割與連接
func TestStringFn(t *testing.T) {s := "A,B,C"parts := strings.Split(s, ",")for _, part := range parts {t.Log(part) //[]string 切片 ["A", "B", "C"]}t.Log(strings.Join(parts, "-"))//strings.Join(parts, "-") 用連字符連接切片元素//輸出A-B-C
}// 字符串與數字轉換
func TestConv(t *testing.T) {s := strconv.Itoa(10) //strconv.Itoa(10) 將整數 10 轉換為字符串 "10"t.Log("str" + s) //然后與 "str" 拼接,得到 "str10"if i, err := strconv.Atoi("10"); err == nil {t.Log(10 + i)}//strconv.Atoi("10") 將字符串轉換為整數,使用 if 語句檢查錯誤,確保轉換成功后才進行計算
}
下面逐個解釋每個測試函數的內容:
func TestString(t *testing.T) { ... }
:測試字符串的基本操作。
聲明一個字符串變量 s
,輸出其默認零值;將字符串賦值為 “hello”,輸出字符串長度;嘗試修改字符串的某個字符,但會報錯,因為字符串是不可變;使用字符串存儲二進制數據和 Unicode 編碼;使用字符串存儲一個中文字符,并輸出其長度;將字符串轉換為 rune
類型切片,輸出切片長度和中文字符的 Unicode 和 UTF-8 編碼。
func TestStringToRune(t *testing.T) { ... }
:測試字符串到 rune
的轉換。
聲明一個包含中文字符的字符串 s
,通過 range
遍歷將字符串轉換為 rune
類型并輸出。
func TestStringFn(t *testing.T) { ... }
:測試字符串相關的函數。
聲明一個包含逗號分隔的字符串 s
,使用 strings.Split
函數拆分字符串并輸出每個部分。使用 strings.Join
函數將拆分的部分合并為一個新的字符串,并輸出。
func TestConv(t *testing.T) { ... }
:測試字符串與其他類型的轉換。
使用 strconv.Itoa
將整數轉換為字符串;拼接字符串和整數,并輸出結果;使用 strconv.Atoi
將字符串轉換為整數,并進行加法運算,處理錯誤情況。
這些測試函數展示了Go語言中字符串的各種操作,包括字符串長度、UTF-8 編碼、rune
類型轉換、字符串拆分和合并,以及字符串與其他類型的轉換。
測試輸出:
Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestString$ go_code/chapter/string=== RUN TestStringd:\Microsoft VS Code\GOproject\src\go_code\chapter\string\string_test.go:11:d:\Microsoft VS Code\GOproject\src\go_code\chapter\string\string_test.go:13: 5d:\Microsoft VS Code\GOproject\src\go_code\chapter\string\string_test.go:24: 亻�d:\Microsoft VS Code\GOproject\src\go_code\chapter\string\string_test.go:25: 4d:\Microsoft VS Code\GOproject\src\go_code\chapter\string\string_test.go:28: 3d:\Microsoft VS Code\GOproject\src\go_code\chapter\string\string_test.go:32: 1d:\Microsoft VS Code\GOproject\src\go_code\chapter\string\string_test.go:34: 中 unicode 4e2dd:\Microsoft VS Code\GOproject\src\go_code\chapter\string\string_test.go:35: 中 UTF8 e4b8ad
--- PASS: TestString (0.00s)
PASS
ok go_code/chapter/string 0.084sRunning tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestStringToRune$ go_code/chapter/string=== RUN TestStringToRuned:\Microsoft VS Code\GOproject\src\go_code\chapter\string\string_test.go:42: 中 4e2dd:\Microsoft VS Code\GOproject\src\go_code\chapter\string\string_test.go:42: 華 534ed:\Microsoft VS Code\GOproject\src\go_code\chapter\string\string_test.go:42: 人 4ebad:\Microsoft VS Code\GOproject\src\go_code\chapter\string\string_test.go:42: 民 6c11d:\Microsoft VS Code\GOproject\src\go_code\chapter\string\string_test.go:42: 共 5171d:\Microsoft VS Code\GOproject\src\go_code\chapter\string\string_test.go:42: 和 548cd:\Microsoft VS Code\GOproject\src\go_code\chapter\string\string_test.go:42: 國 56fd
--- PASS: TestStringToRune (0.00s)
PASS
ok go_code/chapter/string 0.065sRunning tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestStringFn$ go_code/chapter/string=== RUN TestStringFnd:\Microsoft VS Code\GOproject\src\go_code\chapter\string\string_test.go:55: Ad:\Microsoft VS Code\GOproject\src\go_code\chapter\string\string_test.go:55: Bd:\Microsoft VS Code\GOproject\src\go_code\chapter\string\string_test.go:55: Cd:\Microsoft VS Code\GOproject\src\go_code\chapter\string\string_test.go:57: A-B-C
--- PASS: TestStringFn (0.00s)
PASS
ok go_code/chapter/string 0.064sRunning tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestConv$ go_code/chapter/string=== RUN TestConvd:\Microsoft VS Code\GOproject\src\go_code\chapter\string\string_test.go:65: str10d:\Microsoft VS Code\GOproject\src\go_code\chapter\string\string_test.go:67: 20
--- PASS: TestConv (0.00s)
PASS
ok go_code/chapter/string 0.061s
函數
在Go語言中,函數是一種用于執行特定任務的代碼塊,可以被多次調用。
print -eg :
fmt.Printf
和 fmt.Println
是 Go 語言中兩個最常用的格式化輸出函數
函數 | 是否自動換行 | 示例 |
---|---|---|
Printf | 否 | 需顯式加 \n |
Println | 是 | 自動在末尾添加換行 |
fmt.Printf
:
- 需要格式字符串作為第一個參數
- 使用占位符指定輸出格式(如
%s
,%d
,%v
等)
%d 十進制整數
%f 浮點數
%s 字符串
%v 值的默認格式
%+v 顯示結構體字段名
%#v Go語法表示
%T 類型
%% 百分號
-
fmt.Println
-
- 自動格式化,不需要占位符
- 在多個參數間自動添加空格,末尾自動加換行
## 函數基本用法
函數的聲明
在Go中,函數的聲明由關鍵字 func
開始,后面跟著函數名、參數列表、返回值和函數體。
func functionName(parameters) returnType {// 函數體// 可以包含多個語句return returnValue
}
函數參數
函數可以有零個或多個參數,參數由參數名和參數類型組成。參數之間使用逗號分隔。
func greet(name string) {fmt.Printf("Hello, %s!\n", name)
}
多返回值
Go語言的函數可以返回多個值。返回值用括號括起來,逗號分隔。
func divide(a, b float64) (float64, error) {if b == 0 {return 0, errors.New("division by zero")}return a / b, nil //nil 表示沒有錯誤
}
命名返回值
函數可以聲明命名的返回值,在函數體內可以直接使用這些名稱進行賦值,最后不需要顯式使用 return
關鍵字。
func divide(a, b float64) (result float64, err error) {if b == 0 {err = errors.New("division by zero")return}result = a / breturn
}
可變數量的參數
Go語言支持使用 ...
語法來表示可變數量的參數。這些參數在函數體內作為切片使用。
func sum(numbers ...int) int {total := 0for _, num := range numbers {total += num}return total
}
匿名函數和閉包
Go語言支持匿名函數,也稱為閉包。這些函數可以在其他函數內部定義,并訪問外部函數的變量。
func main() {x := 5fn := func() {fmt.Println(x) // 閉包訪問外部變量}fn() // Output: 5
}
defer語句
defer
語句用于延遲執行函數,通常用于在函數返回前執行一些清理操作。
func main() {defer fmt.Println("World")fmt.Println("Hello")
}
以上是一些關于Go語言函數的基本知識點。函數在Go中扮演著非常重要的角色,用于組織代碼、實現功能模塊化和提高代碼的可維護性。
基本使用用例驗證
創建func_basic_test.go練習:
package basicimport ("errors""fmt""testing"
)// 普通函數
func greet(name string) {fmt.Printf("Hello,%s\n", name)
}// 多返回值函數
func divide(a, b int) (int, error) {if b == 0 {return 0, errors.New("division by 0")}return a / b, nil
}// 命名返回函數值
func divideNamed(a, b int) (int, error) {if b == 0 {return 0, errors.New("division by 0")}return a / b, nil
}// 可變數量的參數函數
func sum(numbers ...int) int {total := 0for _, num := range numbers {total += num}return total
}// 函數作為參數
func applyFunction(fn func(int, int) int, a, b int) int {return fn(a, b)
}// 匿名函數和閉包
func closureExample() {x := 5fn := func() {fmt.Println(x)}fn()
}// defer語句
func deferExample() {defer fmt.Println("World")fmt.Println("Hello")//輸出為:Hello World
}//講解:
// 遇到 defer 語句時,注冊fmt.Println("World") 到 defer 棧
// 執行 fmt.Println("Hello")
// 函數返回前,逆序執行 defer 棧中的調用func TestBasic(t *testing.T) {greet("Hzy")//optput:Hello Hzyq, err := divide(10, 2)if err != nil {fmt.Println("Error:", err)} else {fmt.Println("Quotient:", q)}//optput: 5qNamed, errNamed := divideNamed(10, 0)if errNamed != nil {fmt.Println("Error:", errNamed)} else {fmt.Println("Quotitent:", qNamed)}//output: Errors:division by 0total := sum(1, 2, 3, 4, 5)fmt.Println("Sum:", total) //output: Sum:15addResult := applyFunction(func(a, b int) int {return a + b}, 3, 4)fmt.Println("Addition:", addResult) //output: Addition:7closureExample() //5deferExample()//Hello//World
}
測試輸出:
Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestBasic$ go_code/chapter/func=== RUN TestBasic
Hello,Hzy
Quotient: 5
Error: division by 0
Sum: 15
Addition: 7
5
Hello
World
--- PASS: TestBasic (0.00s)
PASS
ok go_code/chapter/func 0.395s
## 業務舉例
創建func_biz_test.go練習,假設你正在開發一個簡單的訂單處理系統,需要計算訂單中商品的總價和應用折扣。你可以使用函數來處理這些業務邏輯。以下是一個簡單的示例:
package bizimport ("fmt""testing"
)type Product struct {Name stringPrice float64
}// 定義了商品的基本數據結構
// Name 字段表示商品名稱(字符串類型)
// Price 字段表示商品價格(浮點數類型)
// 這種結構化的數據表示便于擴展(未來可添加庫存、描述等字段)func calculateTotal(products []Product) float64 {total := 0.0for _, p := range products {total += p.Price}return total
}// 接收一個 Product 切片作為輸入
// 遍歷所有商品,累加價格到 total 變量
// 返回計算出的總價(float64 類型)
//實現購物車商品總價計算的核心邏輯func applyDiscount(amount, discount float64) float64 {return amount * (1 - discount)
}// 接收原始金額和折扣率(0-1之間的小數)
// 計算折扣后金額:原價 × (1 - 折扣率)
// 返回折扣后金額
// 數學關系明確:如 10% 折扣傳入 0.1func TestBiz(t *testing.T) {products := []Product{{Name: "Product A", Price: 10.0},{Name: "Product B", Price: 20.0},{Name: "Product C", Price: 30.0},}// 商品A:$10// 商品B:$20// 商品C:$30total := calculateTotal(products)fmt.Printf("Total before discount: $%2.f\n", total)// 計算原始總價:// 10 + 20 + 30 = $60discountedTotal := applyDiscount(total, 0.1)fmt.Printf("Total after 10%% discount : $%.2f\n", discountedTotal)// 應用10%折扣:// 60 × (1 - 0.1) = $54
}
輸出結果:
Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestBiz$ go_code/chapter/func=== RUN TestBiz
Total before discount: $60
Total after 10% discount : $54.00
--- PASS: TestBiz (0.00s)
PASS
ok go_code/chapter/func 0.450s
面向對象編程
Go語言支持面向對象編程(Object-Oriented Programming,OOP),盡管與一些傳統的面向對象編程語言(如Java和C++)相比,Go的實現方式可能略有不同。在Go語言中,沒有類的概念,但可以通過結構體和方法來實現面向對象的特性。
結構體的定義
在Go語言中,結構體是一種自定義的數據類型,用于組合不同類型的字段(成員變量)以創建一個新的數據類型。編寫struct_test.go,以下是結構體的定義、使用和驗證示例:
package _structimport ("fmt""testing"
)// 定義一個結構體
type Person struct {FirstName stringLastName stringAge int
}func TestStruct(t *testing.T) {// 創建結構體實例并初始化字段personal := Person{FirstName: "Alice",LastName: "Smith",Age: 10,}// 訪問結構體字段fmt.Println("FirstName:", personal.FirstName)fmt.Println("LastName:", personal.LastName)fmt.Println("Age:", personal.Age)// 修改結構體字段的值personal.Age = 7fmt.Println("Updated age:", personal.Age)
}
測試輸出:
Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestStruct$ go_code/chapter2/struct=== RUN TestStruct
FirstName: Alice
LastName: Smith
Age: 10
Updated age: 7
--- PASS: TestStruct (0.00s)
PASS
ok go_code/chapter2/struct 0.472s
結構體的定義可以包含多個字段,每個字段可以是不同的數據類型。
你還可以在結構體中嵌套其他結構體,形成更復雜的數據結構。編寫struct_cmpx_test.go示例:
package _structimport ("fmt""testing"
)type Address struct {Street stringCity stringZipCode string
}type PersonNew struct {FirstName stringLastName stringAge intAddress Address
}func TestCmpxStruct(t *testing.T) {person2 := PersonNew{FirstName: "Bob",LastName: "Johnson",Age: 30,Address: Address{Street: "123 Main St",City: "Cityville",ZipCode: "12345",},}fmt.Println("Full Name:", person2.FirstName, person2.LastName)fmt.Println("Address:", person2.Address.Street, person2.Address.City, person2.Address.ZipCode)
}
測試輸出:
Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestCmpxStruct$ go_code/chapter/struct/cmpx=== RUN TestCmpxStruct
Full Name: Bob Johnson
Address: 123 Main St Cityville 12345
--- PASS: TestCmpxStruct (0.00s)
PASS
ok go_code/chapter/struct/cmpx (cached)
實例創建及初始化
在Go語言中,可以通過多種方式來創建和初始化結構體實例。創建creatinit目錄,以下是幾種常見的實例創建和初始化方法,具體代碼為creatinit_test.go
- 字面量初始化:可以使用花括號
{}
來初始化結構體實例的字段。 - 部分字段初始化: 如果你只想初始化結構體的部分字段,可以省略其他字段。
- 使用字段名初始化: 可以根據字段名來指定字段的值,無需按順序初始化。
- 默認值初始化: 結構體的字段可以根據其類型的默認值進行初始化。
- 使用 new 函數: 可以使用
new
函數來創建一個指向結構體的指針,并返回其指針。 - 字段順序初始化: 可以選擇性地省略字段名,但是這時候需要按照結構體字段的順序進行賦值。
package creatinitimport ("fmt""testing"
)type Person struct {FirstName stringLastName stringAge int
}// 字面量初始化
func TestCreate0bj1(t *testing.T) {person1 := Person{FirstName: "zhangsan",LastName: "Lis",Age: 11,}fmt.Println(person1.FirstName, person1.LastName, person1.Age)
}
//輸出:zhangsan Lis 11func TestCreateObj2(t *testing.T) {person2 := Person{FirstName: "Bob",Age: 30,}fmt.Println(person2.FirstName, person2.LastName, person2.Age) // Output: Bob 30
}// 使用字段名初始化
func TestCreate0bj3(t *testing.T) {person3 := Person{LastName: "Job",FirstName: "Chris",Age: 30,}fmt.Println(person3.FirstName, person3.LastName, person3.Age)//output: Chris Job 30
}// 默認值初始化
func TestCreate0bj4(t *testing.T) {var person4 Personfmt.Println(person4.FirstName, person4.LastName, person4.Age)//output: 0
}// 使用New函數
func TestCreate0bj5(t *testing.T) {person5 := new(Person)person5.FirstName = "David"person5.Age = 40fmt.Println(person5.FirstName, person5.LastName, person5.Age)//output:David 40
}// 字段順序初始化
func TestCreate0bj6(t *testing.T) {person6 := Person{"zhangjie", "xiena", 25}fmt.Println(person6.FirstName, person6.LastName, person6.Age)
}
//output: zhangjie xiena 25
行為(方法)定義
在Go語言中,方法是與特定類型相關聯的函數,它可以在這個類型的實例上調用。方法使得類型的操作能夠與該類型的定義放在一起,提高了代碼的可讀性和可維護性。
創建method目錄進行代碼練習,以下是關于Go語言方法的定義、使用和分析:
方法的定義
在Go語言中,方法是通過為函數添加接收者(receiver)來定義的。接收者是一個普通的參數,但它在方法名前放置,用于指定該方法與哪種類型相關聯。創建method_define_test.go
package methodimport ("fmt""testing"
)// 定義了一個 Circle 結構體,表示圓形
type Circle struct {//包含一個字段 Radius 表示半徑Radius float64
}// (c Circle):這是方法的接收者,表示這個方法屬于 Circle 類型
// Area:方法名稱
// float64:返回值類型
func (c Circle) Area() float64 {// 方法體計算圓的面積:πr2return 3.14159 * c.Radius * c.Radius
}func TestMethodDef(t *testing.T) {//創建 Circle 實例,半徑設為 5c := Circle{Radius: 5}area := c.Area()fmt.Printf("Circle area : %.2f\n", area)
}//eg:
// 方法是與特定類型關聯的函數
// 普通函數不屬于任何類型
在上述示例中,我們定義了一個 Circle
結構體,然后為其定義了一個名為 Area
的方法。這個方法可以通過 c.Area()
的方式調用,其中 c
是一個 Circle
類型的實例。
輸出結果:
Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestMethodDef$ go_code/chapter/struct/method=== RUN TestMethodDef
Circle area : 78.54
--- PASS: TestMethodDef (0.00s)
PASS
ok go_code/chapter/struct/method 0.429s
方法的調用
方法調用的語法為 實例.方法名()
,即通過實例來調用方法。創建method_rpc_test.go
package methodimport ("fmt""testing"
)type Rectangle struct {Width float64Height float64
}func (r Rectangle) Area() float64 {return r.Width * r.Height
}func TestMethodRpc(t *testing.T) {rect := Rectangle{Width: 3, Height: 4}area := rect.Area()fmt.Printf("Rectangle area : %.2f\n", area)
}
指針接收者
Go語言支持使用指針作為方法的接收者,這樣可以修改接收者實例的字段值。創建method_rec_test.go
package methodimport ("fmt""testing"
)type Counter struct {Count int
}//(c *Counter):指針接收者
// 表示這個方法操作的是原結構體的指針
//方法內部可以直接修改結構體字段的值
func (c *Counter) Increment() {c.Count++
}func TestMethodRec(t *testing.T) {counter := Counter{Count: 10}counter.Increment()fmt.Println("Count:", counter.Count)
}
在上述示例中,Increment
方法使用了指針接收者,這樣調用方法后,Count
字段的值會被修改。
測試輸出:
Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestMethodRec$ go_code/chapter/struct/method=== RUN TestMethodRec
Count: 11
--- PASS: TestMethodRec (0.00s)
PASS
ok go_code/chapter/struct/method (cached)
方法與函數的區別
方法與函數的主要區別在于方法是特定類型的函數,它與類型的關系更加緊密,可以訪問類型的字段和其他方法。函數則是獨立于特定類型的代碼塊。方法通常用于實現特定類型的行為,而函數可以用于通用的操作。
通過定義方法,你可以使類型的操作更加自然和一致,提高代碼的可讀性和模塊化。
這里可以說明一下,method_rpc_test.go中,我們為 Rectangle
結構體定義了一個名為 Area
的方法,該方法可以通過 rect.Area()
的方式調用。方法直接與類型 Rectangle
關聯,可以訪問 Rectangle
的字段(Width
和 Height
)。
我們為了與方法作對比,在對應方法體中創建一個方法如下:
// 定義一個函數來計算矩形的面積
func CalculateArea(r Rectangle) float64 {return r.Width * r.Height
}
在這個示例中,我們定義了一個名為 CalculateArea
的函數,它接受一個 Rectangle
類型的參數來計算矩形的面積。函數是獨立于 Rectangle
類型的,因此它無法直接訪問 Rectangle
的字段。
總結: **方法與函數的區別在于方法是特定類型的函數,與類型的關系更加緊密,可以訪問類型的字段和其他方法。而函數是獨立于特定類型的代碼塊,通常用于通用的操作。**在上述示例中,方法與矩形相關聯,可以直接訪問矩形的字段;函數則是一個獨立的計算過程,不與任何特定類型直接關聯。
通過使用方法,我們可以使代碼更加自然和一致,提高代碼的可讀性和模塊化,特別是在實現特定類型的行為時。
接口定義使用
在Go語言中,接口是一種定義方法集合的方式,它規定了一組方法的簽名,而不涉及實現細節。通過接口,可以實現多態性和代碼解耦,使不同類型的對象能夠按照一致的方式進行操作。
定義接口
接口是一組方法的集合,通過 type
關鍵字定義。接口定義了一組方法簽名,但不包含方法的實現。創建interface_test.go進行代碼練習
package interfac_testimport ("fmt""testing"
)// 定義一個簡單的接口
type Shape interface {Area() float64
}
// Shape 是接口名稱
// 只包含一個方法簽名 Area() float64
// 任何實現了 Area() float64 方法的類型都隱式實現了 Shape 接口
// Go 的接口是隱式實現的,不需要顯式聲明// 定義兩個實現Shape接口的結構體
// 圓形實現
type Circle struct {Radius float64
}func (c Circle) Area() float64 {return 3.14159 * c.Radius * c.Radius
}// 矩形實現
type Rectangle struct {Width float64Height float64
}func (r Rectangle) Area() float64 {return r.Width * r.Height
}func TestInterface(t *testing.T) {shapes := []Shape{Circle{Radius: 2},Rectangle{Width: 3, Height: 4},}for _, shape := range shapes {fmt.Printf("Area of %T: %.2f\n", shape, shape.Area())}
}
輸出結果:
Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestInterface$ go_code/chapter/interface=== RUN TestInterface
Area of interfac_test.Circle: 12.57
Area of interfac_test.Rectangle: 12.00
--- PASS: TestInterface (0.00s)
PASS
ok go_code/chapter/interface (cached)
在上面的示例中,我們定義了一個名為 Shape
的接口,該接口要求實現一個 Area
方法,用于計算圖形的面積。然后,我們定義了兩個結構體 Circle
和 Rectangle
,并分別實現了 Area
方法。通過使用接口,我們可以將不同類型的圖形對象放入同一個切片中,然后通過循環調用它們的 Area
方法。
接口的實現
任何類型只要實現了接口中定義的所有方法,就被認為是實現了該接口。接口的實現是隱式的,不需要顯式聲明。只要方法的簽名和接口中的方法簽名相同,類型就被視為實現了接口。
接口的多態性
由于接口的多態性,我們可以將實現了接口的對象視為接口本身。在上面的示例中,shapes
切片中存儲了不同類型的對象,但它們都實現了 Shape
接口,因此可以通過統一的方式調用 Area
方法。
通過使用接口,可以實現代碼的抽象和解耦,使得代碼更加靈活和可擴展。接口在Go語言中被廣泛應用,用于定義通用的行為和約束。
擴展和復用
在Go語言中,擴展和復用代碼的方式與傳統的面向對象語言(如Java)有所不同。Go鼓勵使用組合、接口和匿名字段等特性來實現代碼的擴展和復用,而不是通過類繼承。
創建extend目錄用于后續練習,以下是關于Go語言中擴展和復用的詳細講解:
組合和嵌套
Go語言中的組合(composition)允許你將一個結構體類型嵌套在另一個結構體類型中,從而實現代碼的復用。嵌套的結構體可以通過字段名直接訪問其成員。創建composition_test.go
package compositionimport ("fmt""testing"
)type Engine struct {Model string
}type Car struct {EngineBrand stringPerson string
}func TestComposition(t *testing.T) {car := Car{Engine: Engine{Model: "V8s"},Brand: "XiaomiSu7Ultra",Person: "雷軍",}fmt.Println("Car Brand:", car.Brand)fmt.Println("Car engine model:", car.Model)fmt.Println("Car Father:", car.Person)//直接訪問嵌套結構體字段
}
測試輸出:
Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestComposition$ go_code/chapter/composition=== RUN TestComposition
Car Brand: XiaomiSu7Ultra
Car engine model: V8s
Car Father: 雷軍
--- PASS: TestComposition (0.00s)
PASS
ok go_code/chapter/composition 0.385s
在這個示例中,我們使用了組合來創建 Car
結構體,其中嵌套了 Engine
結構體。通過嵌套,Car
結構體可以直接訪問 Engine
結構體的字段。
接口實現
通過接口,可以定義一組方法,然后不同的類型可以實現這些方法。這樣可以實現多態性和代碼解耦,使得不同類型的對象可以通過相同的接口進行操作。創建interface_ext_test.go
package extendimport ("fmt""math""testing"
)// 定義Shape接口
type Shape interface {Area() float64Perimeter() float64
}// 定義Circle結構體
type Circle struct {Radius float64
}// 實現Circle結構體的方法,以滿足Shape接口
func (c Circle) Area() float64 {return math.Pi * c.Radius * c.Radius//math.Pi = 3.14....//計算面積
}func (c Circle) Perimeter() float64 {return 2 * math.Pi * c.Radius//2pair-->計算周長
}// 定義Rectangle結構體
type Rectangle struct {Width float64Height float64
}// 實現Rectangle結構體的方法,以滿足Shape接口
func (r Rectangle) Area() float64 {return r.Height * r.Width//計算面積
}func (r Rectangle) Perimeter() float64 {return 2 * (r.Height + r.Width)//計算周長
}// 測試函數
// 多態處理:統一接口操作
func TestInterfaceExt(t *testing.T) {circle := Circle{Radius: 3}rectangle := Rectangle{Width: 4, Height: 5}shapes := []Shape{circle, rectangle}for _, shape := range shapes {fmt.Printf("Share Type: %T\n", shape)fmt.Printf("Area: %2.f\n", shape.Area())fmt.Printf("Perimeter: %.2f\n", shape.Perimeter())fmt.Println("------------------")}
}//深層理解:
// 接口不是用來傳遞數據到結構體,而是定義一組行為規范。
// 它更像是一份"能力證書",規定了實現者必須具備哪些能力(方法)
測試輸出:
Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestInterfaceExt$ go_code/chapter/interface/extend=== RUN TestInterfaceExt
Shape Type: extend.Circle
Area: 28.27
Perimeter: 18.85
------------
Shape Type: extend.Rectangle
Area: 20.00
Perimeter: 18.00
------------
--- PASS: TestInterfaceExt (0.00s)
PASS
ok go_code/chapter/interface/extend 0.388s
在上述示例中,我們定義了一個名為 Shape 的接口,它有兩個方法 Area() 和 Perimeter(),分別用于計算形狀的面積和周長。然后,我們分別實現了 Circle 和 Rectangle 結構體的這兩個方法,使它們滿足了 Shape 接口。
通過將不同類型的形狀實例放入一個 []Shape 切片中,我們可以使用統一的方式調用 Area() 和 Perimeter() 方法,實現了代碼的多態性和解耦。這樣,無論我們后續添加新的形狀,只要它們實現了 Shape 接口的方法,就可以無縫地集成到計算器中。
匿名字段和方法重用
通過使用匿名字段,一個結構體可以繼承另一個結構體的字段和方法。創建other_test.go
package extendimport ("fmt""testing"
)type Animal struct {Name string
}func (a Animal) Speak() {fmt.Println("Animal speaks")
}type Dog struct {AnimalBreed string
}func TestOtherExt(t *testing.T) {dog := Dog{Animal: Animal{Name: "liuzi"},Breed: "Folden Retriever",}fmt.Println("Dog name", dog.Name)dog.Speak() //繼承了Animal的Speak的用法//輸出:Animal speaks
}
測試輸出:
Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestOtherExt$ go_code/chapter/interface/extend=== RUN TestOtherExt
Dog name liuzi
Animal speaks
--- PASS: TestOtherExt (0.00s)
PASS
ok go_code/chapter/interface/extend 0.399s
在上述示例中,Dog
結構體嵌套了 Animal
結構體,從而繼承了 Animal
的字段和方法。
通過這些方式,你可以在Go語言中實現代碼的擴展和復用。盡管Go不像傳統的面向對象語言那樣強調類繼承,但通過組合、接口和匿名字段等特性,你仍然可以實現類似的效果,使代碼更靈活、可讀性更高,并保持低耦合性。
空接口和斷言
空接口和斷言是Go語言中用于處理不確定類型和類型轉換的重要概念。
創建emptyassert目錄用于后續練習,下面是關于空接口和斷言的學習總結:
空接口(Empty Interface)
空接口是Go語言中最基礎的接口,它不包含任何方法聲明。因此,空接口可以用來表示任何類型的值。空接口的聲明方式為 interface{}
。
空接口的主要用途是在需要處理不確定類型的場景中。通過使用空接口,可以接受和存儲任何類型的值,類似于其他編程語言中的動態類型。但需要注意的是,使用空接口可能會導致類型安全性降低,因為編譯時無法檢查具體類型。
斷言(Type Assertion)
斷言是一種在空接口中恢復具體類型的機制,它允許我們在運行時檢查空接口中的值的實際類型,并將其轉換為相應的類型。斷言的語法為 value.(Type)
,其中 value
是接口值,Type
是要斷言的具體類型。
創建emptyassert_test.go進行驗證:
package emptyassertimport ("fmt""testing"
)func DoSomething(p interface{}) {switch v := p.(type) {//通過類型斷言檢查 p 的具體類型,v 是轉換后的具體類型值。case int:fmt.Println("Integer", v)case string:fmt.Println("String", v)default:fmt.Println("Unknow Type")}
}func TestEmptyInterfaceAssertion(t *testing.T) {//傳參檢測DoSomething(10)DoSomething("10")
}func TestEmptyAssert(t *testing.T) {var x interface{} = "hello"//安全類型斷言,檢查 x 是否為 string 類型str, ok := x.(string)if ok {fmt.Println("String", str)} else {fmt.Println("Not a string")}
}// 總結:
// 空接口:interface{} 可存儲任意類型的值,但需類型斷言或類型切換來訪問具體值。
// 類型切換(Type Switch):簡化多類型判斷,自動提取具體類型的值。
// 安全類型斷言:使用 value, ok := x.(T) 避免斷言失敗時的 panic。
// 測試覆蓋:現有測試用例覆蓋了 int 和 string,但未測試 default 分支(如傳入 float64 會觸發)。
下面逐個解釋每個測試函數的內容:
func DoSomething(p interface{}) { ... }
:定義了一個函數 DoSomething
,該函數接受一個空接口參數 p
,然后根據接口值的實際類型進行類型斷言,根據不同的類型輸出不同的信息。
func TestEmptyInterfaceAssertion(t *testing.T) { ... }
:測試空接口的斷言操作。
調用 DoSomething(10)
,將整數 10
傳遞給函數,函數根據類型斷言輸出整數類型信息。調用 DoSomething("10")
,將字符串 "10"
傳遞給函數,函數根據類型斷言輸出字符串類型信息。
func TestEmptyAssert(t *testing.T) { ... }
:測試空接口的類型斷言操作。
聲明一個空接口變量 x
,并將字符串 "hello"
賦值給它。使用類型斷言 x.(string)
判斷 x
是否為字符串類型,如果是,將其賦值給變量 str
,并輸出字符串值;否則輸出 “Not a string”。
這些測試函數展示了Go語言中空接口的斷言操作,通過類型斷言可以判斷空接口中的具體類型,并執行相應的操作。
測試輸出:
Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestEmptyInterfaceAssertion$ go_code/chapter/interface/emptyassert=== RUN TestEmptyInterfaceAssertion
Integer 10
String 10
--- PASS: TestEmptyInterfaceAssertion (0.00s)
PASS
ok go_code/chapter/interface/emptyassert (cached)Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestEmptyAssert$ go_code/chapter/interface/emptyassert=== RUN TestEmptyAssert
String hello
--- PASS: TestEmptyAssert (0.00s)
PASS
ok go_code/chapter/interface/emptyassert 0.156s
總結: 空接口和斷言是Go語言中處理不確定類型和類型轉換的強大工具。空接口允許存儲任何類型的值,而斷言允許我們在運行時檢查和轉換接口值的實際類型。使用這些機制,可以在需要處理不同類型的值時實現更靈活和通用的代碼。但在使用空接口和斷言時,要注意維護類型安全性,并進行適當的錯誤處理。
GO 接口最佳實踐
在Go語言中,使用接口的最佳實踐可以提高代碼的可讀性、可維護性和靈活性。
- 小接口與大接口: 盡量設計小接口,一個接口應該只包含少量的方法,而不是設計一個大而全的接口。這樣可以避免實現接口時不必要的負擔,并使接口更具通用性。
- 基于使用場景設計接口: 設計接口時應該考慮使用場景,而不是從具體的實現出發。思考在你的應用程序中如何使用接口,以及接口應該提供哪些方法來滿足這些使用場景。
- 使用合適的命名: 為接口和方法使用清晰的命名,使其能夠表達出其用途和功能。命名應該具有可讀性和表達性,讓其他開發者能夠輕松理解接口的用途。
- 避免不必要的接口: 不要為每個類型都創建一個接口,只有在多個類型之間確實存在共享的行為和功能時才使用接口。不要過度使用接口,以免導致不必要的復雜性。
- 使用接口作為函數參數和返回值: 使用接口作為函數參數和返回值,可以使函數更加通用,允許傳入不同類型的參數,并返回不同類型的結果。這可以提高代碼的復用性和擴展性。
- 注釋和文檔: 為接口提供清晰的文檔和注釋,說明接口的用途、方法的功能和預期行為。這可以幫助其他開發者更好地理解接口的使用方式。
- 用例驅動設計: 在設計接口時,可以從使用的角度出發,先考慮接口在實際場景中如何被調用,然后再設計接口的方法和簽名。
- 將接口的實現與定義分離: 將接口的實現與接口的定義分開,這樣可以使實現更靈活,可以在不修改接口定義的情況下實現新的類型。
- 默認實現: 在接口定義中,可以為某些方法提供默認實現,從而減少實現接口時的工作量。這對于可選方法或者某些方法的默認行為很有用。
- 使用空接口謹慎: 使用空接口(
interface{}
)應謹慎,因為它會降低類型安全性。只有在確實需要處理不同類型的值時才使用空接口,同時要注意類型斷言和錯誤處理。
設計和使用接口時要根據實際需求和項目的特點來選擇合適的方案。
錯誤類型
基本使用介紹
錯誤類型
在Go中,錯誤被表示為一個實現了 error
接口的類型。error
接口只有一個方法,即 Error() string
,它返回一個描述錯誤的字符串。
type error interface {Error() string
}
返回錯誤值
當一個函數遇到錯誤情況時,通常會返回一個錯誤值。這個錯誤值可以是一個實現了 error
接口的自定義類型,也可以是Go標準庫中預定義的錯誤類型,如 errors.New()
創建的錯誤。
錯誤檢查
調用者通常需要顯式地檢查函數返回的錯誤,以判斷是否發生了錯誤。這可以通過在調用函數后使用 if
語句來實現。
以上兩個直接寫代碼如下:
package basicimport ("errors""fmt""testing"
)// 以 Err 開頭,定義兩種錯誤類型,用于參數校驗。
var ErrLessThanTwoError = errors.New("n should be not less than 2")
var ErrLargerThenHundredError = errors.New("n should be not larger than 100")// 斐波那契生成模塊
func GetFibonacci(n int) ([]int, error) {//參數校驗if n < 2 {return nil, ErrLessThanTwoError}if n > 100 {return nil, ErrLargerThenHundredError}//生成數列,生成前 n 個斐波那契數。fibList := []int{1, 1}// 若 n < 2 或 n > 100 返回對應錯誤。for i := 2; i < n; i++ {fibList = append(fibList, fibList[i-2]+fibList[i-1])}return fibList, nil// 初始化數列為 [1, 1]。// 從第3個元素開始(索引2),每個元素是前兩個元素之和。// 例如:n=5 時,生成 [1, 1, 2, 3, 5]。
}func TestGetFibonacci(t *testing.T) {// 測試 n=1(觸發 ErrLessThanTwo)if v, err := GetFibonacci(1); err != nil {if err == ErrLessThanTwoError {fmt.Println("It is less.")}t.Error(err)} else {t.Log(v)}}
測試,當n=1時:
Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestGetFibonacci$ go_code/chapter/basic=== RUN TestGetFibonacci
It is less.d:\Microsoft VS Code\GOproject\src\go_code\chapter\basic\error_test.go:43: n should be not less than 2
--- FAIL: TestGetFibonacci (0.00s)
FAIL
FAIL go_code/chapter/basic 0.170s
測試,當n=5時:
Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestGetFibonacci$ go_code/chapter/basic=== RUN TestGetFibonaccid:\Microsoft VS Code\GOproject\src\go_code\chapter\basic\error_test.go:45: [1 1 2 3 5]
--- PASS: TestGetFibonacci (0.00s)
PASS
ok go_code/chapter/basic (cached)
測試,當n=101時:
Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestGetFibonacci$ go_code/chapter/basic=== RUN TestGetFibonaccid:\Microsoft VS Code\GOproject\src\go_code\chapter\basic\error_test.go:43: n should be not larger than 100
--- FAIL: TestGetFibonacci (0.00s)
FAIL
FAIL go_code/chapter/basic 0.174s
錯誤鏈
在某些情況下,錯誤可以包含附加信息,以便更好地理解錯誤的原因。可以通過 fmt.Errorf()
函數來創建包含附加信息的錯誤。
假設我們正在構建一個文件操作的庫,其中包含文件讀取和寫入功能。有時,在文件讀取或寫入過程中可能會出現各種錯誤,例如文件不存在、權限問題等。我們希望能夠提供有關錯誤的更多上下文信息。
package chainimport ("errors""fmt""testing"
)type FileError struct {Op string //操作類型("read"/"write")Path string //文件路徑錯誤Err error //原始錯誤
}// 實現error接口的Error()方法
func (e *FileError) Error() string {return fmt.Sprintf("%s %s:%v", e.Op, e.Path, e.Err)
}// 結構體包含三個字段:Op(操作類型)、Path(文件路徑)、Err(原始錯誤)。
// 通過實現 Error() string 方法,FileError 滿足 error 接口,可直接作為錯誤返回。
// 錯誤信息格式示例:read /path/to/file.txt:file not found。// 模擬文件讀取操作
func ReadFile(path string) ([]byte, error) {// 模擬文件不存在的情況return nil, &FileError{Op: "read", Path: path, Err: errors.New("file not found")}
}//函數返回一個自定義的 FileError,包裝了底層錯誤 file not found
//通過 &FileError{...} 創建指針類型,確保錯誤類型可被正確識別func TestChain(t *testing.T) {filePath := "/path/to/file.txt"_, err := ReadFile(filePath)if err != nil {fmt.Println("Error:", err)// 在這里,我們可以檢查錯誤類型,提取上下文信息// 類型斷言解包錯誤if fileErr, ok := err.(*FileError); ok {fmt.Printf("Operation:%s\n", fileErr.Op)fmt.Printf("File Path:%s\n", fileErr.Path)fmt.Printf("Original Error:%v\n", fileErr.Err)}}
}// 流程:
// 調用 ReadFile 觸發錯誤。
// 打印完整錯誤信息(Error: read /path/to/file.txt:file not found)。
// 通過類型斷言(err.(*FileError))提取自定義錯誤中的詳細信息。
// 輸出操作類型、文件路徑和原始錯誤。
下面是代碼的解釋:
FileError
結構體:定義了一個自定義錯誤類型 FileError
,包含以下字段:
Op
:操作類型,表示是讀取(“read”)還是寫入(“write”)操作;Path
:文件路徑,表示涉及哪個文件;Err
:原始錯誤,包含底層的錯誤信息。
Error()
方法:為 FileError
結構體實現了 error
接口的 Error()
方法,用于生成錯誤的文本描述。
ReadFile()
函數:模擬文件讀取操作。在這個示例中,該函數返回一個 FileError
類型的錯誤,模擬了文件不存在的情況。
TestChain()
測試函數:演示如何在錯誤處理中使用自定義錯誤類型。
定義了一個文件路徑 filePath
,并調用 ReadFile(filePath)
函數來模擬文件讀取操作;檢查錯誤,如果發生錯誤,輸出錯誤信息;在錯誤處理中,通過類型斷言檢查錯誤是否為 *FileError
類型,如果是,則可以提取更多上下文信息,如操作類型、文件路徑和原始錯誤信息。
個人理解:
“當執行文件讀取操作后,若檢測到文件路徑不存在,函數會 返回 nil
作為數據占位符(表示無有效內容),并通過返回一個指向自定義錯誤 FileError
的指針來傳遞錯誤詳情。調用方通過檢查 err != nil
來判斷是否發生錯誤,并通過類型斷言提取錯誤上下文(如操作類型、文件路徑等)。”
測試輸出:
Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestChain$ go_code/chapter/error/chain=== RUN TestChain
Error: read /path/to/file.txt:file not found
Operation:read
File Path:/path/to/file.txt
Original Error:file not found
--- PASS: TestChain (0.00s)
PASS
ok go_code/chapter/error/chain 0.449s
Panic 和 Recover
在Go語言中,panic
和 recover
是用于處理異常情況的機制,但它們應該謹慎使用,僅用于特定的情況,而不是替代正常的錯誤處理機制。以下是對 panic
和 recover
的詳細解釋,并給出一個具體用例:
panic
創建panic
目錄,編寫panic
_test.go。
panic
是一個內置函數,用于引發運行時恐慌。當程序遇到無法繼續執行的致命錯誤時,可以使用 panic
來中斷程序的正常流程。但應該避免濫用 panic
,因為它會導致程序崩潰,不會提供友好的錯誤信息。典型情況下,panic
用于表示程序中的不可恢復錯誤,例如切片索引越界。
package panicimport ("fmt""testing"
)// 通過主動觸發 panic 避免程序因未處理的越界訪問而崩潰。
func TestPanic(t *testing.T) {arr := []int{1, 2, 3} // 定義一個切片,長度3,,有效索引為0-2index := 4 //越界索引// 主動檢查索引是否越界if index >= len(arr) {panic("Index out of range") // 觸發自定義 panic}element := arr[index]fmt.Println("Element:", element) // 若未觸發 panic,嘗試訪問元素
}
在上述示例中,如果索引 index
超出了切片 arr
的范圍,會觸發 panic
,導致程序崩潰。這種情況下,panic
用于表示程序的不可恢復錯誤。
測試輸出:
Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestPanic$ go_code/chapter/error/panic=== RUN TestPanic
--- FAIL: TestPanic (0.00s)
panic: Index out of range [recovered]panic: Index out of rangegoroutine 19 [running]:
testing.tRunner.func1.2({0x2f6880, 0x3594a0})C:/Go/src/testing/testing.go:1632 +0x225
testing.tRunner.func1()C:/Go/src/testing/testing.go:1635 +0x359
panic({0x2f6880?, 0x3594a0?})C:/Go/src/runtime/panic.go:785 +0x132
go_code/chapter/error/panic.TestPanic(0xc0000d84e0?)d:/Microsoft VS Code/GOproject/src/go_code/chapter/error/panic/panic_test.go:15 +0x25
testing.tRunner(0xc0000d84e0, 0x32f7b0)C:/Go/src/testing/testing.go:1690 +0xcb
created by testing.(*T).Run in goroutine 1C:/Go/src/testing/testing.go:1743 +0x377
FAIL go_code/chapter/error/panic 0.415s
當index在正確索引時:
//當index=2
Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestPanic$ go_code/chapter/error/panic=== RUN TestPanic
Element: 3
--- PASS: TestPanic (0.00s)
PASS
ok go_code/chapter/error/panic (cached)
recover
創建recover
目錄,編寫recover
_test.go。recover
也是一個內置函數,用于恢復 panic
引發的運行時恐慌。它只能在延遲函數(defer
)內部使用,并且用于恢復程序的控制流,而不是用于處理錯誤。通常,在發生 panic
后,recover
可以在延遲函數中捕獲 panic
,并執行一些清理工作,然后程序會繼續執行。//So,這是聯用的啊。
package recoverimport ("fmt""testing"
)// 清理函數:捕獲 panic 并恢復
func cleanup() {if r := recover(); r != nil { // 捕獲 panic 的值fmt.Println("Recoverd from panic:", r) // 處理錯誤(注意拼寫應為 "Recovered")}
}// 測試函數:觸發 panic 并恢復
func TestRecover(t *testing.T) {defer cleanup() // 注冊延遲執行的清理函數panic("Something went wrong") // 主動觸發 panicfmt.Println("This line will not be executed") // 此代碼不會執行
}
在上述示例中,panic
觸發后,cleanup
函數中的 recover
捕獲了 panic
,并打印了錯誤消息。然后程序會繼續執行,但需要注意的是,控制流不會回到觸發 panic
的地方,因此 fmt.Println
不會被執行。
總之,panic
和 recover
應該謹慎使用,只用于特殊情況,如不可恢復的錯誤或在延遲函數中進行清理操作。在大多數情況下,應該優先使用錯誤返回值來處理異常情況,因為這種方式更安全、可控,能夠提供更好的錯誤信息和錯誤處理。只有在特定的情況下,例如遇到不可恢復的錯誤時,才應該考慮使用 panic
和 recover
。
測試輸出:
Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestRecover$ go_code/chapter/error/recover=== RUN TestRecover
Recoverd from panic: Something went wrong
--- PASS: TestRecover (0.00s)
PASS
ok go_code/chapter/error/recover 0.402s
自定義錯誤類型
在Go中,你可以根據需要定義自己的錯誤類型,只需滿足 error
接口的要求即可。這允許你創建更具描述性和上下文的錯誤類型。
在Go中,自定義錯誤類型是一種強大的方式,可以創建更具描述性和上下文的錯誤,以提供更好的錯誤信息。自定義錯誤類型必須滿足 error
接口的要求,即實現 Error() string
方法。以下是一個示例,展示如何自定義錯誤類型和驗證其用例:
package defineimport ("fmt""testing""time"
)// 自定義錯誤類型
type TimeoutError struct {Operation stringTimeout time.Time
}// 實現error接口的Error()方法
func (e TimeoutError) Error() string {return fmt.Sprintf("Timeout error during %s operation.Timeout at %s",e.Operation,e.Timeout.Format("2025-04-24 15:00:00")) // Go 固定時間格式模板
}//封裝超時錯誤的上下文信息
// Timeout 記錄具體的超時時間點。
// 實現 Error() string 方法,滿足 error 接口,提供可讀的錯誤信息。// 模擬執行某個操作,可能會超時
func PerformOperation() error {timeout := time.Now().Add(-5 * time.Second) // 設置超時時間為當前時間 -5 秒,模擬超時if time.Now().After(timeout) { // 判斷當前時間是否已超過超時時間點return TimeoutError{Operation: "PerformOperation", Timeout: timeout}//模擬成功}return nil
}func TestDefineError(t *testing.T) {err := PerformOperation()if err != nil {// 檢查錯誤類型并打印錯誤信息if timeoutError, ok := err.(TimeoutError); ok {fmt.Println("Error type:", timeoutError.Operation)fmt.Println("Timeout at:", timeoutError.Timeout)}fmt.Println("Error:", err)} else {fmt.Println("Operation completed successfully.")}
}// 調用 PerformOperation,預期返回錯誤
// 錯誤處理:
// 使用類型斷言 err.(TimeoutError) 檢查錯誤類型。
// 若為 TimeoutError,提取并打印操作名稱和超時時間。
// 最后打印完整錯誤信息(通過 Error() 方法)。
// 無錯誤時:打印成功信息。
輸出結果:
Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestDefineError$ go_code/chapter/error/define=== RUN TestDefineError
Error type: PerformOperation
Timeout at: 2025-04-24 13:19:26.2072294 +0800 CST m=-4.998300499
Error: Timeout error during PerformOperation operation.Timeout at 242426-19-2419 13:00:00
--- PASS: TestDefineError (0.02s)
PASS
ok go_code/chapter/error/define 0.433s
下面是代碼的解釋:
TimeoutError
結構體:定義了一個自定義錯誤類型 TimeoutError
,包含以下字段:
Operation
:操作名稱,表示哪個操作超時;Timeout
:超時時間,表示操作發生超時的時間點。
Error()
方法:為 TimeoutError
結構體實現了 error
接口的 Error()
方法,用于生成錯誤的文本描述。
PerformOperation()
函數:模擬執行某個操作,可能會超時。在這個示例中,如果當前時間超過了超時時間,則返回一個 TimeoutError
類型的錯誤。
TestDefineError()
測試函數:演示如何在錯誤處理中使用自定義錯誤類型。
調用 PerformOperation()
函數來模擬操作,并檢查是否發生了錯誤;如果發生錯誤,首先檢查錯誤類型是否為 TimeoutError
,如果是,則提取超時操作和超時時間,并輸出相關信息;最后,無論是否發生錯誤,都會輸出錯誤信息或成功完成的消息。
這個示例展示了如何自定義錯誤類型以及如何在錯誤處理中利用這些自定義錯誤類型來提供更多的上下文信息,使錯誤處理更加有信息和靈活。在這里,TimeoutError
提供了有關超時操作和超時時間的額外信息。
包和依賴管理
src目錄下創建chapter7,Go 語言的包和依賴管理主要通過其內置的模塊系統(Go Modules)來實現。Go Modules 于 Go 1.11 版本首次引入,并在 Go 1.13 版本中成為默認的依賴管理方式。
package
(包)的基本知識點
基本復用模塊單元
在 Go 語言中,package
是代碼復用的基本單元。一個 package
可以包含多個 Go 源文件,這些文件可以共享同一個包中的代碼,并通過包的導入機制被其他包使用。
包的可見性:在 Go 語言中,通過首字母大寫來表明一個標識符(如變量、函數、類型等)可以被包外的代碼訪問。反之,首字母小寫的標識符只能在包內使用。
// mypackage.go
package mypackage// 公有函數,其他包可以訪問
func PublicFunction() {// 實現細節
}// 私有函數,僅在當前包內可訪問
func privateFunction() {// 實現細節
}
代碼的 package
可以和所在的目錄不一致
Go 語言的文件組織結構鼓勵但不強制 package
名稱與其所在目錄名稱一致。通常情況下,開發者會遵循這種約定以保持代碼的一致性和可讀性,但 Go 并不強制執行這一規則。
實際應用:你可以在chapter2目錄下創建多個文件,并在這些文件中定義相同的包名 mypackage
,也可以選擇一個不同于目錄名的包名。
// chapter2/1.go
package mypackage// chapter2/2.go
package mypackage
同一目錄里的 Go 代碼的 package
要保持一致
在同一目錄中的所有 Go 文件必須聲明相同的 package
名稱。這是 Go 語言的一個基本規則,確保同一目錄下的所有文件都屬于同一個包,從而能夠互相訪問這些文件中聲明的標識符。
違例情況:如果你在同一目錄下使用不同的 package
名稱,Go 編譯器將會報錯,提示包聲明不一致。
構建一個自身可復用的package
src目錄下創建chapter2后,再次新建series,編寫my_series.go如下:
package seriesimport "fmt"//當其他代碼通過 import "series" 導入該包時
//會依次打印,用于初始化
func init() {fmt.Println("init1")
}func init() {fmt.Println("init2")
}//接收一個整數 n,返回其平方值。
func Square(n int) int {return n * n
}//生成斐波那契數列
func GetFibonacciSerie(n int) []int {//處理 n < 2 的情況if n <= 0 {return []int{}}ret := []int{1, 1}if n <= 2 {return ret[:n]}//生成包含前 n 項的斐波那契數列。for i := 2; i < n; i++ {ret = append(ret, ret[i-2]+ret[i-1])}return ret
}
然后在chapter2中新建client,編寫package_test.go將上面的內容引入:
package clientimport ("go_code/chapter2/series""testing"
)func TestPackage(t *testing.T) {t.Log(series.GetFibonacciSerie(5)) //輸出結果為:[1,1,2,3,5]t.Log(series.Square(5)) //輸出結果為:25
}
測試輸出結果:
Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestPackage$ go_code/chapter2/clientinit1
init2
=== RUN TestPackaged:\Microsoft VS Code\GOproject\src\go_code\chapter2\client\package_test.go:9: [1 1 2 3 5]d:\Microsoft VS Code\GOproject\src\go_code\chapter2\client\package_test.go:10: 25
--- PASS: TestPackage (0.00s)
PASS
ok go_code/chapter2/client 0.445s
通過在 chapter2
目錄下創建一個名為 series
的包,把與數學相關的函數(如求平方和斐波那契數列)集中在一起。這樣在其他地方需要使用這些功能時,只需引入這個包即可,不必重復編寫相同的代碼。
知識點:包的初始化
- 利用 Go 語言中的
init()
函數機制進行包的初始化操作。在 Go 中,每個包可以有多個init()
函數,這些函數會在包第一次被加載時自動執行,且執行順序按照代碼順序。 - 在
series
包中編寫了兩個init()
函數,它們會在包被引入時自動執行。這種機制可以用于在包加載時執行一些必要的初始化工作(如設置默認值、加載配置等),或者用來調試包的加載過程。
導入和應用遠程依賴(即外部包)
獲取和更新遠程依賴
- 使用
go get
命令來下載并添加遠程依賴到項目中。Go Modules 會自動管理這些依賴,并更新go.mod
和go.sum
文件。 - 如果需要強制從網絡獲取最新版本的依賴,可以使用
-u
參數:- 示例:
go get -u github.com/user/repo
這將更新指定包及其依賴項到最新的次要版本或修訂版本。
- 示例:
代碼在 GitHub 上的組織形式
- 確保代碼庫的目錄結構直接反映包的導入路徑,而不要使用
src
目錄作為根目錄。這使得項目更容易與 Go 的依賴管理工具兼容,確保導入路徑的簡潔和一致性。
github.com/username/project/
├── mypackage/
│ └── mypackage.go
└── anotherpackage/└── anotherpackage.go
- 最佳實踐:在 GitHub 上組織代碼時,目錄結構應與包名匹配,例如:
- 這樣可以避免導入路徑中的多余層級,并確保使用
go get
時能正確定位包。
按照該思路我們進行驗證,在在 chapter7
目錄下創建一個名為 remote_package
的包,我們先進行下載**“go get github.com/easierway/concurrent_map”**的下載,然后創建remote_package_test.go進行驗證:
package remoteimport ("fmt""testing"// 并發安全Map庫(別名cm)cm "github.com/easierway/concurrent_map"
)func TestConcurrentMap(t *testing.T) {// 1. 創建并發Map(分片數=99)m := cm.CreateConcurrentMap(99)// 2. 插入鍵值對m.Set(cm.StrKey("key"), 10)// 3. 查詢操作value, ok := m.Get(cm.StrKey("key"))// 4. 結果斷言if ok {fmt.Println("Key found:", value) // 控制臺輸出(測試時慎用)t.Log(m.Get(cm.StrKey("key"))) // 推薦:記錄到測試日志}
}
測試輸出:
Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestConcurrentMap$ go_code/chapter2/package/remote=== RUN TestConcurrentMap
Key found: 10d:\Microsoft VS Code\GOproject\src\go_code\chapter2\package\remote\remote_test.go:16: 10 true
--- PASS: TestConcurrentMap (0.00s)
PASS
ok go_code/chapter2/package/remote 0.091s
concurrent_map的介紹:concurrent_map
是一個由 GitHub 用戶 easierway
創建的 Go 包,主要用于實現線程安全的并發 map 數據結構。這個包提供了一種簡單且高效的方式來處理并發環境下的 map 操作,避免了傳統 map 在多 goroutine 訪問時出現的競爭問題。
功能/特點 | 說明 |
---|---|
線程安全 | 通過**分段鎖機制(分片鎖)**確保 map 在多 goroutine 并發訪問時的數據安全。 |
高效的讀寫操作 | 將 map 分成多個子 map,減少鎖的粒度,提高并發訪問的效率。 |
簡單易用的 API | 提供類似標準 map 的接口,如 Set 、Get 、Remove ,使用方式簡單。 |
動態擴展 | 根據使用需求動態擴展或收縮分段,提高資源利用率。 |
包的依賴管理
Go 語言在早期的依賴管理中(使用 GOPATH)確實存在一些未解決的問題:
同一環境下,不同項目使用同一包的不同版本
在 Go Modules 引入之前,Go 的依賴管理依賴于
GOPATH
目錄。所有的項目共享同一個GOPATH
,這就導致了一個問題:如果兩個項目需要使用同一包的不同版本,由于GOPATH
中同一個包只能有一個版本,無法同時滿足這兩個項目的需求。這種情況下,開發者往往需要手動管理和切換包版本,帶來了很大的麻煩和不確定性。無法管理對包的特定版本的依賴
在沒有 Go Modules 之前,Go 的依賴管理缺乏對包版本的精確控制。通常情況下,開發者只能獲取最新版本的包,這就導致了以下問題:
- 當某個包發布了不兼容的新版本時,項目可能會因自動升級到新版本而導致編譯或運行錯誤。
- 難以重現歷史版本的構建,因為無法確定項目依賴的具體版本。
Go Modules 如何解決這些問題
為了解決這些問題,Go 從 1.11 版本開始引入了 Go Modules,從根本上改變了 Go 的依賴管理方式。Go Modules 提供了版本控制和模塊隔離的機制,避免了上述問題。
不同項目使用同一包的不同版本
- 獨立的模塊空間:每個 Go 項目通過
go.mod
文件獨立管理其依賴關系。go.mod
文件定義了項目所依賴的所有包及其版本,這些包會被下載到$GOPATH/pkg/mod
下,并且是根據模塊名和版本號來隔離的。因此,不同項目可以使用同一包的不同版本,而不會相互干擾。 - 無需全局
GOPATH
:Go Modules 擺脫了對全局GOPATH
的依賴,轉而使用模塊級的依賴管理。每個項目的依賴包版本在項目目錄下獨立管理,避免了版本沖突。
管理對包的特定版本的依賴
- 精確的版本控制:在
go.mod
文件中,你可以指定依賴包的具體版本。Go Modules 支持語義化版本控制(Semantic Versioning),你可以通過@
符號指定某個依賴包的版本號(如v1.2.3
),或者使用go get <package>@<version>
命令來更新某個依賴的版本。這樣,你可以明確指定和鎖定項目依賴的版本,確保項目的可重現性。 - 版本兼容性和依賴解析:Go Modules 通過
go.mod
和go.sum
文件管理版本依賴,確保項目構建過程中使用的依賴版本是可預測且穩定的。即使某個依賴包發布了新版本,你的項目仍會使用go.mod
中指定的版本,除非你主動升級。
雖然 Go Modules 解決了許多依賴管理問題,但它也帶來了一些新的挑戰:
- 多模塊項目的管理:在一些大型項目中,可能會有多個模塊,這些模塊之間的依賴管理需要謹慎處理,特別是當這些模塊之間存在依賴關系時。
- 依賴沖突:如果不同的依賴項依賴于同一個包的不同版本,Go Modules 會嘗試找到一個可用的共同版本,但這可能并不總是理想的解決方案。
Go Modules 通過模塊化和版本控制,基本解決了 Go 語言早期依賴管理中的主要問題,如同一環境下不同項目使用同一包的不同版本,以及對包的特定版本的依賴管理問題。然而,盡管如此,隨著項目規模的擴大和依賴關系的復雜化,依賴管理仍然需要開發者謹慎對待。
編寫相同的代碼。
知識點:包的初始化
- 利用 Go 語言中的
init()
函數機制進行包的初始化操作。在 Go 中,每個包可以有多個init()
函數,這些函數會在包第一次被加載時自動執行,且執行順序按照代碼順序。 - 在
series
包中編寫了兩個init()
函數,它們會在包被引入時自動執行。這種機制可以用于在包加載時執行一些必要的初始化工作(如設置默認值、加載配置等),或者用來調試包的加載過程。
導入和應用遠程依賴(即外部包)
獲取和更新遠程依賴
- 使用
go get
命令來下載并添加遠程依賴到項目中。Go Modules 會自動管理這些依賴,并更新go.mod
和go.sum
文件。 - 如果需要強制從網絡獲取最新版本的依賴,可以使用
-u
參數:- 示例:
go get -u github.com/user/repo
這將更新指定包及其依賴項到最新的次要版本或修訂版本。
- 示例:
代碼在 GitHub 上的組織形式
- 確保代碼庫的目錄結構直接反映包的導入路徑,而不要使用
src
目錄作為根目錄。這使得項目更容易與 Go 的依賴管理工具兼容,確保導入路徑的簡潔和一致性。
github.com/username/project/
├── mypackage/
│ └── mypackage.go
└── anotherpackage/└── anotherpackage.go
- 最佳實踐:在 GitHub 上組織代碼時,目錄結構應與包名匹配,例如:
- 這樣可以避免導入路徑中的多余層級,并確保使用
go get
時能正確定位包。
按照該思路我們進行驗證,在在 chapter7
目錄下創建一個名為 remote_package
的包,我們先進行下載**“go get github.com/easierway/concurrent_map”**的下載,然后創建remote_package_test.go進行驗證:
package remoteimport ("fmt""testing"// 并發安全Map庫(別名cm)cm "github.com/easierway/concurrent_map"
)func TestConcurrentMap(t *testing.T) {// 1. 創建并發Map(分片數=99)m := cm.CreateConcurrentMap(99)// 2. 插入鍵值對m.Set(cm.StrKey("key"), 10)// 3. 查詢操作value, ok := m.Get(cm.StrKey("key"))// 4. 結果斷言if ok {fmt.Println("Key found:", value) // 控制臺輸出(測試時慎用)t.Log(m.Get(cm.StrKey("key"))) // 推薦:記錄到測試日志}
}
[外鏈圖片轉存中…(img-lTFriOCz-1746605136776)]
測試輸出:
Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestConcurrentMap$ go_code/chapter2/package/remote=== RUN TestConcurrentMap
Key found: 10d:\Microsoft VS Code\GOproject\src\go_code\chapter2\package\remote\remote_test.go:16: 10 true
--- PASS: TestConcurrentMap (0.00s)
PASS
ok go_code/chapter2/package/remote 0.091s
concurrent_map的介紹:concurrent_map
是一個由 GitHub 用戶 easierway
創建的 Go 包,主要用于實現線程安全的并發 map 數據結構。這個包提供了一種簡單且高效的方式來處理并發環境下的 map 操作,避免了傳統 map 在多 goroutine 訪問時出現的競爭問題。
功能/特點 | 說明 |
---|---|
線程安全 | 通過**分段鎖機制(分片鎖)**確保 map 在多 goroutine 并發訪問時的數據安全。 |
高效的讀寫操作 | 將 map 分成多個子 map,減少鎖的粒度,提高并發訪問的效率。 |
簡單易用的 API | 提供類似標準 map 的接口,如 Set 、Get 、Remove ,使用方式簡單。 |
動態擴展 | 根據使用需求動態擴展或收縮分段,提高資源利用率。 |
包的依賴管理
Go 語言在早期的依賴管理中(使用 GOPATH)確實存在一些未解決的問題:
同一環境下,不同項目使用同一包的不同版本
在 Go Modules 引入之前,Go 的依賴管理依賴于
GOPATH
目錄。所有的項目共享同一個GOPATH
,這就導致了一個問題:如果兩個項目需要使用同一包的不同版本,由于GOPATH
中同一個包只能有一個版本,無法同時滿足這兩個項目的需求。這種情況下,開發者往往需要手動管理和切換包版本,帶來了很大的麻煩和不確定性。無法管理對包的特定版本的依賴
在沒有 Go Modules 之前,Go 的依賴管理缺乏對包版本的精確控制。通常情況下,開發者只能獲取最新版本的包,這就導致了以下問題:
- 當某個包發布了不兼容的新版本時,項目可能會因自動升級到新版本而導致編譯或運行錯誤。
- 難以重現歷史版本的構建,因為無法確定項目依賴的具體版本。
Go Modules 如何解決這些問題
為了解決這些問題,Go 從 1.11 版本開始引入了 Go Modules,從根本上改變了 Go 的依賴管理方式。Go Modules 提供了版本控制和模塊隔離的機制,避免了上述問題。
不同項目使用同一包的不同版本
- 獨立的模塊空間:每個 Go 項目通過
go.mod
文件獨立管理其依賴關系。go.mod
文件定義了項目所依賴的所有包及其版本,這些包會被下載到$GOPATH/pkg/mod
下,并且是根據模塊名和版本號來隔離的。因此,不同項目可以使用同一包的不同版本,而不會相互干擾。 - 無需全局
GOPATH
:Go Modules 擺脫了對全局GOPATH
的依賴,轉而使用模塊級的依賴管理。每個項目的依賴包版本在項目目錄下獨立管理,避免了版本沖突。
管理對包的特定版本的依賴
- 精確的版本控制:在
go.mod
文件中,你可以指定依賴包的具體版本。Go Modules 支持語義化版本控制(Semantic Versioning),你可以通過@
符號指定某個依賴包的版本號(如v1.2.3
),或者使用go get <package>@<version>
命令來更新某個依賴的版本。這樣,你可以明確指定和鎖定項目依賴的版本,確保項目的可重現性。 - 版本兼容性和依賴解析:Go Modules 通過
go.mod
和go.sum
文件管理版本依賴,確保項目構建過程中使用的依賴版本是可預測且穩定的。即使某個依賴包發布了新版本,你的項目仍會使用go.mod
中指定的版本,除非你主動升級。
雖然 Go Modules 解決了許多依賴管理問題,但它也帶來了一些新的挑戰:
- 多模塊項目的管理:在一些大型項目中,可能會有多個模塊,這些模塊之間的依賴管理需要謹慎處理,特別是當這些模塊之間存在依賴關系時。
- 依賴沖突:如果不同的依賴項依賴于同一個包的不同版本,Go Modules 會嘗試找到一個可用的共同版本,但這可能并不總是理想的解決方案。
Go Modules 通過模塊化和版本控制,基本解決了 Go 語言早期依賴管理中的主要問題,如同一環境下不同項目使用同一包的不同版本,以及對包的特定版本的依賴管理問題。然而,盡管如此,隨著項目規模的擴大和依賴關系的復雜化,依賴管理仍然需要開發者謹慎對待。