文章目錄
- 第一部分:一腳踢你進Go語言大門!
-
- Ⅰ、基礎不牢,地動山搖
-
- 1.第一個例子:Hello World
- 2.Go 環境搭建
-
- 2.1環境變量
- 3.項目結構
- 4.編譯釋出
- 5.跨平台編譯
- Ⅱ、資料類型
-
- 1. 都有哪些類型
-
- 變量聲明
- 整型
- 浮點數
- 布爾型
- 字元串
- 零值
- 2.變量的簡短聲明
- 3.指針
- 4.常量
-
- 常量定義
- 5.iota
- 6.字元串
- Ⅲ、控制結構
-
- 1. if 條件語句
- 2. switch 選擇語句
- 3. for 循環語句
- Ⅳ、集合類型
-
- 1. Array(數組)
-
- 1.1數組聲明
- 1.2 數組循環
- 2. 切片
-
- 2.1數組生成切片
- 2.2 切片修改
- 2.3切片聲明
- 2.3 Append
-
- 2.4切片循環
- 3. Map (映射)
-
- 3.1 Map 聲明初始化
- 3.2 Map 擷取、删除
- 4. 周遊 Map
-
- 4.1 Map 的大小
- 5. String 和 []byte
- Ⅴ、函數和方法
-
- 1. 函數
-
- 1.1 函數聲明
- 1.2 包級函數
- 1.3 匿名函數和閉包
- 2. 方法
- 3. 值類型接收者、指針類型接收者
- 4. 方法表達式
- Ⅵ、struct 和 interface
-
- 1. 結構體
-
- 1.1 定義
- 1.2 聲明
- 1.3字段結構體
- 2. 接口
-
- 2.1 定義
- 2.2 接口的實作
- 2.3 使用
- 3. 值接受者、指針接受者
- Ⅶ、錯誤處理,error 和 panic
-
- 1. 錯誤
-
- 1.1 error 接口
- 1.2 error 工廠函數
- 1.3 自定義 error
- 1.4 error 斷言
- 2. Panic 異常
-
- 2.1 Recover 捕獲 Panic 異常
- Ⅷ、斷言和反射
-
- 1. 接口斷言
-
- 1.1 文法格式:
- 示例
- 2. 反射
-
- 2.1 反射有何用
- 2.1 reflect 包
- 第二部分:Go 的高效并發程式設計執行個體
您諸位好啊,我是無塵!
第一部分:一腳踢你進Go語言大門!
Ⅰ、基礎不牢,地動山搖
1.第一個例子:Hello World
package main
import "fmt"
func main(){
fmt.Println("Hello World")
}
第一行 package main 代表目前的檔案屬于哪個包,package 是 go 語言生命包的關鍵字,main 是包名,main包是一個特殊的包,代表此項目為一個可運作的應用程式,而不是一個被其他項目引用的庫。
第二行 import “fmt” 是導入一個 fmt 包,import 是關鍵字
第三行 func main(){} 定義了一個函數,func 是關鍵字,main 是函數名,mian 是一個特殊函數,代表整個程式的入口,程式在運作時,會點調用 main 函數。
第四行 fmt.Println(“Hello World”) 是通過 fmt 包的 Println 函數列印 “Hello World”文本。
2.Go 環境搭建
可以從官網 https://golang.org/dl/ (國外官網)和 https://golang.google.cn/dl/ (國内官網)下載下傳Go語言開發包。
2.1環境變量
- GOPATH:Go 項目的工作目錄,現在有了 Go Module 模式,是以基本上用來放使用 go get 指令擷取的項目
- GOBIN:Go 編譯生成的程式安裝目錄,比如
指令 會把生成的go 程式安裝到 GOBIN 目錄下,以供終端使用。go install
- 若工作目錄為 /Users/wucs/go,需要把 GOPATH 環境變量設定為 /Users/wucs/go,把 GOBIN 環境變量設定為 $GOPATH/bin
- Linux/macOS 下,把以下内容添加到 /etc/profile 或者 $HOME/.profile 檔案儲存即可:
export GOPATH=/Users/wucs/go export GOBIN=$GOPATH/bin
3.項目結構
我們采用 Go Module 模式開進行開發,此模式不必将代碼放在GOPATH目錄中,可以在任意位置來建立項目。
- 比如項目位置為 \golang\gotour,打開終端,切換到項目目錄,然後執行
,會生成一個 go.mod 檔案。然後在項目根目錄建立 main.go 檔案。go mod init example.com/hello
go mod 是Golang 1.11 版本引入的官方包(package)依賴管理工具,用于解決之前沒有地方記錄依賴包具體版本的問題,友善依賴包的管理。
-
初始化子產品。go mod init “module名字”
-
增加缺失的包,移除沒用的包go mod tidy
-
- 将文章開始的 Hello World 執行個體寫入到 main.go 檔案中。
main.go 就是整個項目的入口檔案,裡面有mian函數。
4.編譯釋出
- 在項目根目錄執行
,會在項目根目錄生成 main.exe 檔案go build ./main.go
- 在項目根目錄下,終端輸入
回車,成功列印 “Hello World”,說明程式成功運作。main
- 以上生成的可執行檔案在項目根目錄,也可以把它安裝到 $GOBIN目錄或者其他任意位置:
go install /main.go
go install 指令可以将程式生成在$GOBIN目錄,現在可以在任意位置打開終端,輸入mian 回車,都會列印 “Hello World”。
5.跨平台編譯
什麼是跨平台編譯?比如你在windows下開發,可以編譯在linux上運作的程式。
Go 語言通過兩個環境變量來控制跨平台編譯,它們分别是 GOOS 和 GOARCH 。
- GOOS:代表要編譯的目标作業系統,常見的有 Linux、Windows、Darwin 等。
- GOARCH:代表要編譯的目标處理器架構,常見的有 386、AMD64、ARM64 等
macOS AMD64下開發,編譯 linux AMD64 程式:
GOOS=linux GOARCH=amd64 go build ./main.go
關于 GOOS 和 GOARCH 更多的組合,參考官方文檔的 $GOOS and $GOARCH 這一節。
Ⅱ、資料類型
1. 都有哪些類型
變量聲明
- var 變量名 類型 = 表達式
var i int = 10
- 類型推導
可以根據值的類型來省略變量類型var i = 10
- 聲明多個變量
var (
i int = 0
k int = 1
)
// 同理類型推導
var (
i = 0
k = 1
)
類型int/float64/bool/string 等基礎類型都可以被自動推導。
整型
在 Go 語言中,整型分為:
- 有符号整型:int、int8、int16、int32、int64
- 無符号整型:uint、uint8、uint16、uint32、uint64
注意:
- 有符号整型可以表示負數、零、正數,而無符号整型隻能為零和正數。
- int 和 uint 這兩個沒有具體的 bit 大小的整型,他們大小可能是32bit,也可能是64bit,這個取決于硬體裝置CPU。
- 在整型中,如果能确定int的bit就使用明确的int類型,這一有助于程式的移植性。
- 還有一種位元組類型 byte,它其實等價于 uint8,可以了解為 uint8 類型的别名,用于定義一個位元組,是以位元組byte類型也屬于整型。
浮點數
浮點數就是含有小數的數字,Go語言中提供了兩種精度的浮點數:float32、float64。因為 float64 精度高,浮點計算結果比 float誤差要更小,是以它更被常使用。
布爾型
- 一個布爾值值隻有兩種:true 和 false。
- 定義使用:
;使用 bool 關鍵字定義var bf bool = false
字元串
字元串通過類型 string 聲明
var s1 string = "hello"
var s2 = "world" //類型推導
var s3 = s1 + s2 //可以通過操作符 + 把字元串串連起來
s1 += s2 //也可以通過 += 運算符操作
零值
零值其實就是一個變量的預設值,Go語言中,如果隻聲明了一個變量,并沒有對其指派,那麼此變量會有一個對應類型的零值。
var b bool // bool型零值是false
var s string // string的零值是""
以下六種類型零值常量都是nil
var a *int
var a []int
var a map[string] int
var a chan int
var a func(string) int
var a error // error是接口
2.變量的簡短聲明
變量名:=表達式
在實際項目中,如果能為聲明的變量初始化,那麼就使用簡短的聲明方式,這種也是使用最多的。
3.指針
Go 語言中,指針對應的是變量在記憶體中存儲的位置,也就是說指針的值就是周遊的記憶體位址。通過 & 可以擷取變量的位址,也就是指針。*可以擷取位址對應的值。
pi:=&i
fmt.Println(*pi)
4.常量
常量的值是在編譯期就确定好的,确定後不能被修改,可以防止在運作期被惡意篡改。
常量定義
和變量類型,隻不過使用關鍵字 const
const name = "無塵"
在 Go語言中,隻允許布爾型、字元串、數字類型這些基礎類型作為常量。
5.iota
iota 是一個常量生成器,可以用來初始化相似規則的常量,避免重複的初始化。
const (
one = 1
two = 2
three = 3
)
//使用 iota
const (
one = iota+1
two
three
)
iota 的初始值是0。
6.字元串
- 字元串和數字互換
Go是強類型語言,不同類型的變量是不能互相使用和計算的。不同類型的變量在進行複制或計算時,需要先進行類型轉換。
i := 10
itos := strconv.Itoa(i)
stoi,err := strconv.Atoi(itos)
fmt.Println(itos,stoi,err) //10 10 nil
- String 包
string 包是Go SDK提供的一個标準包。用于處理字元串的工具包。包含查找字元串、拆分字元串、去除字元串的空格、判斷字元串是否含有某個字首或字尾。
//判斷s1的字首是否是H
fmt.Println(strings.HasPrefix(s1,"H"))
//在s1中查找字元串o
fmt.Println(strings.Index(s1,"o"))
//把s1全部轉為大寫
fmt.Println(strings.ToUpper(s1))
更多例子,可以檢視 string文檔
Ⅲ、控制結構
1. if 條件語句
func main() {
i:=6
if i >10 {
fmt.Println("i>10")
} else if i>5 && i<=10 {
fmt.Println("5<i<=10")
} else {
fmt.Println("i<=5")
}
}
注意:
- if 後的表達無 ‘( )’
每個條件分支中的 ‘{ }’ 是必須的。哪怕隻有一行代碼。
3.if/else後的 ‘{’ 不能獨占一行。否則編譯不通過
2. switch 選擇語句
if 條件語句比較适合分支比較少的情況。如果有很多分支,switch會更友善。
switch i:=6;{
case i > 10:
fmt.Println("i>10")
case i > 6 && i <= 10:
fmt.Println("5<i<10")
default:
fmt.Println("i<=5")
}
注意: Go 語言為防止忘記寫 break,case 後自帶 break,這和其他語言不一樣。
如果确實需要執行下一個 case ,可以使用 fallthrough 關鍵字
switch j:=1;j{
case 1:
fallthrough
case 2:
fmt.Println("1")
default:
fmt.Println("無比對")
}
以上結果會輸出 1。
當switch之後有表達式時,case後的值就要和這個表達式的結果類型相同,比如這裡 j 是 int 類型,是以 case 後就得使用 int 類型。
3. for 循環語句
for 循環由三部分組成,其中需要使用兩個 ; 分割:
sum := 0
for i := 1; i <= 100; i++{
sum += i
}
fmt.Println("sum:",sum)
第一部分是簡單語句
第二部分是 for 循環的條件
第三部分是更新語句
這三部分組成都不是必須的,可以被省略。
Go 語言中沒有 while 循環,可以通過for達到while的效果:
sum := 0
i := 1
for i <= 100 {
sum += 1
i++
}
Go 中,同樣支援continue,break 控制for循環。
- continue 跳出本次循環,進入下次循環。
- break 強行退出整個循環。
Ⅳ、集合類型
1. Array(數組)
數組存放的是固定長度、相同類型的資料。
1.1數組聲明
- var <數組名> = [<長度>]<元素>{元素1,元素2}
或者var arr = [2]int{1,2}
arr := [2]int{1,2}
- var <數組名> = […]<元素類型>{元素1,元素2}
或者var arr = [...]int{1,2}
arr := [...]int{1,2}
- var <數組名> = […]<類型>{索引1:元素1,索引2:元素2}
或者var arr = [...]int{1:1,0:2}
arr := [...]int{1:1,0:2}
數組的每個元素在記憶體中都是連續存放的,每個元素都有一個下标,下标從0開始。
數組長度可以省略,會自動根據{}中的元素來進行推導。
沒有初始化的索引,預設值是數組類型的零值。
1.2 數組循環
for i,v := range array {
fmt.Printf("索引:%d,值:%s\n",i,v)
}
- range 表達式傳回數組索引指派給 i,傳回數組值指派給 v。
- 如果傳回的值用不到,可以用 _ 下劃線丢棄:
for _,v:= range array{
fmt.Printf("值:%s\n",i,v)
}
2. 切片
切片和數組類型,可以了解為動态的數組,切片是基于數組實作的,它的底層就是一個數組。對于數組的分割,便可以得到一個切片。
2.1數組生成切片
slice := array[start:end]
array := [5]string{"a","b","c","d","e"}
slice := array[2:5]
fmt.Println(slice) //[c d e]
注意:這裡包含索引2,但是不包含索引5的元素,即:左閉右開。
經過切片後,切片的索引範圍也改變了。
array[start:end] 中的 start 和 end 都是可以省略的,start 的預設值是 0 ,end 的預設值為數組的長度。
2.2 切片修改
切片的值也可以被修改,這裡也可以證明切片的底層是數組。
array := [5]string{"a","b","c","d","e"}
slice := array[2:5] //[c d e]
slice[1] = "f"
fmt.Println(slice) //[c f e]
fmt.Println(array) //[a b c f e]
修改切片,對應的數組值也被修改了,是以證明基于數組的切片,使用的底層數組還是原來的數組,一旦修改切片的元素值,底層數組對應的值也會被修改。
2.3切片聲明
使用 make 函數聲明切片
//聲明一個元素類型為string的切片,長度是4
slice := make([]string,4)
//長度是4,容量是8
slice1 := make([]srting,4,8)
切片的容量不能比切片長度小。
長度就是元素個數。
容量就是切片的空間。
上面執行個體在記憶體上劃分了一個容量為8的記憶體空間,但是隻是用了4個記憶體空間,剩餘的處于空閑狀态。當通過 append 往切片追加元素時,會追加到空閑記憶體上,剩餘空間不足時,會進行擴容。
字面量初始化切片
slice2 := []string{"a","b","c"}
fmt.Println(len(slice2),cap(slice2)) //3 3
2.3 Append
append 函數對一個切片進行追加元素:
slice3 := append(slice2,"d")
//追加多個元素
slice3 := append(slice2,"d","f")
//追加一個切片
slice3 := append(slice2,slice...)
小技巧:
在建立新切片時,最好讓長度和容量一樣,這樣追加操作的時候就會生成新的底層數組,進而和原有數組分離,就不會因為公用底層數組導緻修改内容的時候影響多個切片。
2.4切片循環
切片循環與數組一樣,也是使用 for range 方式。
3. Map (映射)
map 是一個無序的 k-v 鍵值對集合。其中 k 必須是相同類型。k 和 v 的類型可以不同。 k 的類型必須支援 == 比較運算符,這樣才可以判斷它是否存在,并保證唯一。
3.1 Map 聲明初始化
- make:
mapName := make(map[string]int)
- 字面量:
mapName := map[string]int{"無塵":29}
如果不想建立的時候添加鍵值對,使用空大括号{}即可,切記不能省略。
3.2 Map 擷取、删除
//添加鍵值對或更新對應的key的value
mapName["無塵"] = 20
//擷取指定key的value
age := mapName["無塵"]
擷取不存在的 k-v 鍵值對時,如果 key 不存在,傳回的 value 是該值的零值,是以很多時候,需要先判斷 map 中的 key 是否存在。
nameAge := make([string]int)
nameAge["無塵"]=29
age,ok := nameAge["無塵"]
if ok {
fmt.Println(age)
}
- map 的 [] 操作傳回兩個值
- 第一個是 value
- 第二個是标記該 key 是否存在,存在則為 true
delete()函數進行删除
delete(nameAge,"無塵")
- delete 有兩個參數,一個是map,一個是要删除的 key 。
4. 周遊 Map
nameAge["無塵"] = 29
nameAge["無塵1"] = 30
nameAge["無塵2"] = 31
for k,v := range nameAge{
fmt.Println("key is",k,"value is ",v)
}
- 對應 map ,for range 傳回兩個參數,分别是 k 和 v。
小技巧:for range 周遊 map 的時候,若使用一個傳回值,則這個傳回值是 map 的 key 。
4.1 Map 的大小
map 不同于切片,map 隻有長度,沒有容量。可以使用 len 函數擷取 map 大小。
5. String 和 []byte
字元串也是一個不可變的位元組序列,可以直接轉為位元組切片 []byte :
s:="Hello無塵小生"
bs := []byte(s)
string 不止可以直接轉為 []byte,還可以使用 [] 操作符擷取指定索引的位元組值。
字元串是位元組序列,每一個索引對應一個位元組,在 UTF8 編碼下,一個漢字對應三個位元組。
如果把一個漢字當做一個長度計算,可以使用 utf8.RuneCountInString 函數。
for range 周遊時,是按照 unicode 字元進行循環的,一個漢字占一個長度。
Ⅴ、函數和方法
1. 函數
1.1 函數聲明
func funcName(params) result {
body
}
- 關鍵字 func 用于聲明一個函數
- funcName 函數名
- params 函數的參數
- result 是函數的傳回值,可以傳回多個傳回值,如果沒有可以省略。
- body 函數體
示例
1.
- a、b形參類型一緻,可以省略其中一個類型的聲明
func sum (a, b int) {
return a + b
}
2.多值傳回
- 傳回值的部分類型定義需要小括号括起來。
func sum (a, b int) (int,error) {
if a <0 || b <0 {
return 0, errors.New("a或b不能是負數")
}
return a + b, nil
}
3.命名參數傳回
- 函數中給命名傳回參數指派,相當于函數有了傳回值,是以可以忽略 return 後要傳回的值了。
func sum (a, b int) (sum int,err error) {
if a <0 || b <0 {
return 0, errors.New("a或b不能是負數")
}
sum = a + b
err = nil
return
}
4.可變參數
- 函數的參數是可變的
- 定義可變參數,隻要在參數類型前加三個點 … 即可
- 可變參數的類型其實就是切片,下面示例中 params 的參數類型是 []int
func sum(params ...int) int {
sum := 0
for _, i := range params {
sum += i
}
return sum
}
1.2 包級函數
- 函數都會從屬于一個包,我們自定義的函數屬于 main 包。Println 函數屬于 fmt 包。
- 想要調用其他包内的函數,那麼那個函數名稱首字母要大寫,使其作用域變為公有的。
- 函數首字母小寫,隻能在同一個包中被調用
1.3 匿名函數和閉包
匿名函數就是沒有名稱的函數。
func main(){
//注意,sum 隻是一個函數類型的變量,不是函數名字
sum := func(a, b int) int {
return a + b
}
fmt.Println(sum(1, 2)) // 3
}
匿名函數可以在函數中進行嵌套,這個匿名函數稱為内部函數,内部函數可以使用外部函數的變量,這種方式就是閉包。
func main (){
sm := sum()
fmt.Println(sum())
fmt.Println(sum())
fmt.Println(sum())
}
func sum () func() int{
i := 0
return func ()int{
i++
return i
}
}
//結果為:
1
2
3
由于閉包函數,sum 函數傳回一個匿名函數,匿名函數持有外部函數 sum 的變量 i,是以在main函數中,每次調用 sum(),i的值就會 +1。
在 Go 語言中,函數也是一種類型,可以作為函數類型的變量、參數、或者一個函數的傳回值。
2. 方法
方法和函數類似,不同之處就是方法必須有一個接收者,這個接收者是一個“類”(類型),這樣這個方法就算屬于這個“類”。
type Name string
func (n Name)String(){
fmt.Println("name is ", n)
}
- 示例中 String() 就是 Name 這個類型的方法
- 接收者需要加在 func 和方法名之間,使用()
- 接收者: (變量,類型)
使用:
func main(){
name := Name("無塵")
name.String()
}
//出處
name is 無塵
3. 值類型接收者、指針類型接收者
方法的接收者可以使用值類型(例如上面示例)或者指針類型。
如果接收者是指針,那麼對指針的修改是有效的:
func (n *Name) Modify(){
*n = Name("wucs")
}
func main(){
name := Name("無塵")
name.String()
name.Modify()
name.String()
}
//輸出
name is 無塵
name is wucs
注意:在調用方法時,傳遞的接收者實質上都是副本,隻不過一個是值副本,一個是指向這個值的指針的副本。指針指向原有值,是以修改指針指向的值,也就修改了原有值。
方法的調用者,可以是值,也可以是指針((&name).Modify()),Go 語言會自動轉義,我們無需關心。
4. 方法表達式
方法可以指派給變量
name := Name("無塵")
//方法指派給變量,方法表達式
n = Name.String
//要傳一個接收者name進行調用
n(name)
無論方法是否有參數,通過方法表達式調用,第一個參數必須是接收者,然後才是方法自身的參數。
Ⅵ、struct 和 interface
1. 結構體
1.1 定義
結構體是種聚合類型,裡面可以包含任意類型的值,這些值就是結構體的成員,或成為字段,定義結構體,需要使用 type+struct 關鍵字組合
type person struct { //人結構體
name string //人的名字
age uint //人的年齡
}
- type 與 struct 是關鍵字,用來定義一個新結構體的類型。
- person 為結構體名字。
- name/age 為結構體的字段名,後面指對應的字段類型。
- 字段聲明和變量類似,變量名在前,類型在後
- 字段可以是人一個,一個字段都沒有的結構體,成為空結構體。
- 結構體也是一種類型,比如 person 結構體和 person 類型是一個意思。
1.2 聲明
- 像普通字元串、整型醫院聲明初始化
var p person
聲明了一個person類型的變量p,但是沒有初始化,是以預設使用結構體裡字段的零值。
- 字面量方式初始化
p := person{"無塵",18}
表示結構體變量 p 的name字段初始化為“無塵”,age字段初始化為18。順序必須和字段定義順序一緻。
- 根據字段名稱初始化
p := person{age:18,name:"無塵"}
像這樣指出字段名,就可以打亂初始化字段的順序。也可以隻初始化其中部分字段,剩餘字段預設使用零值: p := person{age:30}
1.3字段結構體
結構體字段可以是任意類型,包括自定義的結構體類型:
type person struct { //人結構體
name string
age uint
addr address //使用自定義結構體類型
}
type address struct { //位址結構體
city string
}
對于這樣嵌套結構體,初始化和一般結構體類似,根據字段對應的類型初始化即可:
p := person {
age:18,
name:"無塵",
addr:address{
city:"北京",
},
}
結構體的字段和調用一個類型的方法一樣,都是使用點操作符“.”:
fmt.Println(p.age)
//通路嵌套結構體裡的city字段的值:
fmt.Println(p.addr.city)
2. 接口
2.1 定義
接口是一個抽象的類型,是和調用方的一種約定。接口隻需要定義約定,告訴掉用方可以做什麼,而不用知道它的内部實作。
接口的定義是 type + interface關鍵字類實作。
//Info 是一個接口,它有方法 Getinfo()string
type Info interface {
Getinfo() string
}
對應 Stringer 接口,它會告訴調用者可以通過 String()放擷取一個字元串,這就是接口的約定,而這個字元串是怎麼擷取到的,接口并不關心,調用者也不用關心,因為這些是接口的實作者來處理的。
2.2 接口的實作
接口的實作者必須是一個具體的類型:
func (p person) Getinfo() string {
return fmt.Sprintf("my name is %s,age is %d",p.name,p.age)
}
- 給結構體類型 person 定義了一個方法,這個方法和接口裡的方法名稱、參數、傳回值都一樣,就表示這個結構體 person 實作了 Info 接口。
- 如果一個接口有多個方法,那麼要實作接口中的所有方法才算是實作了這個接口。
2.3 使用
我們先定義一個可以列印 Info 接口的函數:
func printInfo(i Info) {
fmt.Println(i.Getinfo())
}
- 定義函數 pringInfo,它接收一個 Info 接口類型的參數,然後列印接口 Getinfo 方法傳回的字元串。
- 這個 pringInfo 函數此處是面向接口程式設計,隻有任何一個類型實作了Info接口,都可以使用這個函數列印出對應的字元串,而不用關心具體的類型實作。
printInfo(p)
//結果為:my name is 無塵,age is 18
因為 person 類型實作了Info接口,是以變量p可以作為函數printInfo的參數。
3. 值接受者、指針接受者
- 實作一個接口,必須實作接口中所有的方法。
- 定義一個方法,有值類型接收者和指針類型接收者,兩者都可以調用方法,因為Go編譯器自動做了轉換。
- 但是接口的實作,值類型接收者和指針類型接收者不一樣
上面接口體person實作了Info接口,是否結構體指針也實作了該接口呢?
測試發現p的指針作為參數函數也是可以正常運作,表明以值類型接收者實作接口,類型本身和該類型的指針類型,都實作了該接口
那麼把接收者改成指針類型:
func (p *person) Getinfo() string {
return fmt.Sprintf("my name is %s,age is %d",p.name,p.age)
}
然後再調用函數
printInfo(p)
,代碼編譯不通過,表明以指針類型接收者實作接口,隻有對應的指針類型才被認為實作了接口
方法接收者 | 實作接口類型 |
---|---|
(p person) | person和*person |
(p *person) | *person |
- 當值類型作為接收者,person類型和*person類型都實作了該接口。
- 當指針類型作為接收者,隻有 *person類型實作了該接口。
Ⅶ、錯誤處理,error 和 panic
1. 錯誤
在Go語言中,錯誤并不是非常嚴重,它是可以預期的,可以傳回錯誤給調用者自行處理。
1.1 error 接口
在Go語言中,錯誤是通過内置的error接口來表示的,它隻有一個Error方法來傳回錯誤資訊:
type error interface {
Error() string
}
這裡示範一個錯誤的示例:
func main() {
i,err := strconv.Atoi("a")
if err != nil {
fmt.Println(err)
}else {
fmt.Println(i)
}
}
- 示例故意使用錯誤的字元串“a”來轉為整數,是以這裡會列印錯誤資訊:
strconv.Atoi: parsing "a": invalid syntax
- 一般,error接口在當函數或方法調用時遇到錯誤時進行傳回,且為第二個傳回值,這樣調用者就可以根據錯誤來自行處理。
1.2 error 工廠函數
我們可以使用 errors.New 這個工廠函數來生成錯誤資訊,它接收一個字元串參數,傳回一個error接口。
func test(m,n int) (int, error) {
if m > n {
return m,errors.New("m大于n")
}else {
return n,nil
}
}
當m大約n的情況下,傳回一個錯誤資訊。
1.3 自定義 error
上面工廠函數隻能傳遞一個字元串來傳回,要想攜帶更多資訊,這時候可以使用自定義error:
type testError struct {
errorCode int //錯誤碼
errorMsg string //錯誤資訊
}
func (t *testError) Error() string{
return t.errorMsg
}
這裡自定義error,它可以傳回更多資訊:
return m, &testError{
errorCode: 1,
errorMsg: "m大于n"}
上面通過字面量方式建立*testError 來傳回。
1.4 error 斷言
通過error斷言來擷取傳回的錯誤資訊,斷言可以将error接口轉為自己定義的錯誤類型:
res, err := test(2,1)
if e,ok := err.(*testError);ok {
fmt.Println("錯誤碼:",e.errorCode,",錯誤資訊:",e.errorMsg)
} else {
fmt.Println(res)
}
2. Panic 異常
Go語言是一門靜态語言,很多錯誤可以在編譯的時候進行捕獲,不過對于數組越界通路、不同類型強制轉換這種,會在運作時候才會引起panic異常。
我們也可以手動來抛出 panic 異常,這裡以連接配接mysql資料庫為例:
func connectMySQL(ip,username,password string){
if ip =="" {
panic("ip不能為空")
}
//省略其他代碼
}
- 在以上函數中,如果ip位址為空,會抛出 panic 異常。
- panic 是Go語言内置函數,可以接收 interface{} 類型的參數,也就是說任何類型的值都是可以傳遞給 panic 函數的:
interface{} 表示空接口,代表任意類型。
panic 是一種非常嚴重的錯誤,會使程式中斷執行,是以 如果不是影響程式運作的錯誤,使用 error 即可
2.1 Recover 捕獲 Panic 異常
一般我們不對panic異常做處理,但是如果有一些需要在程式崩潰前做處理的操作,可以使用内置的 recover 函數來恢複 panic 異常。
程式 panic 異常崩潰的時候,隻有defer修飾的函數才會被執行,是以 recover 函數要結合 defer 關鍵字一起使用:
func main() {
defer func() {
if p:=recover();p!=nil{
fmt.Println(p)
}
}()
connectMySQL("","root","123456")
}
recover 函數捕獲了 panic 異常,列印:
recover 函數傳回的值就是通過 panic 函數傳遞的參數值。 ip不能為空
- recover 函數的傳回值就是 panic 函數傳遞的參數值。
- defer 關鍵字修飾的函數,會在主函數退出前被執行。
Ⅷ、斷言和反射
1. 接口斷言
提到接口斷言,我們先回顧下怎麼實作接口?
- 接口的實作者必須是一個具體類型
- 類型定義的方法和接口裡方法名、參數、傳回值都必須一緻
- 若接口有多個方法,那麼要實作接口中的所有方法
對于空接口 interface{} ,因為它沒有定義任何的函數(方法),是以說Go中的所有類型都實作了空接口。
當一個函數的形參是 interface{} 時,意味着這個參數被自動的轉為interface{} 類型,在函數中,如果想得到參數的真實類型,就需要對形參進行斷言。
- 類型斷言就是将接口類型的值x,轉換成類型T,格式為:x.(T)
- 類型斷言x必須為接口類型
- T可以是非接口類型,若想斷言合法,則T必須實作x的接口
1.1 文法格式:
//非安全類型斷言
<目标類型的值> := <表達式>.( 目标類型 )
// 安全類型斷言
<目标類型的值>,<布爾參數> := <表達式>.( 目标類型 )
示例
package main
import "fmt"
func whoAmi(a interface{}) {
//1.不斷言
//程式報錯:cannot convert a (type interface{}) to type string: need type assertion
//fmt.Println(string(a))
//2.非安全類型斷言
//fmt.Println(a.(string)) //無塵
//3.安全類型斷言
value, ok := a.(string) //安全,斷言失敗,也不會panic,隻是ok的值為false
if !ok {
fmt.Println("斷言失敗")
return
}
fmt.Println(value) //無塵
}
func main() {
str := "無塵"
whoAmi(str)
}
斷言還有一種形式,就是使用switch語句判斷接口的類型:
func whoAmi(a interface{}) {
switch a.(type) {
case bool:
fmt.Printf("boolean: %t\n", a) // a has type bool
case int:
fmt.Printf("integer: %d\n", a) // a has type int
case string:
fmt.Printf("string: %s\n", a) // a has type string
default:
fmt.Printf("unexpected type %T", a) // %T prints whatever type a has
}
}
2. 反射
Go語言提供了一種機制,在運作時可以更新和檢查變量的值、調用變量的方法和變量支援的内在操作,但是在編譯時并不知道這些變量的具體類型,這種機制被稱為反射。
2.1 反射有何用
- 上面我們提到空接口,它能接收任何東西
- 但是怎麼來判斷空接口變量存儲的是什麼類型呢?上面介紹的類型斷言可以實作
- 如果想擷取存儲變量的類型資訊和值資訊就需要使用到反射
- 反射就是可以動态擷取變量類型資訊和值資訊的機制
2.1 reflect 包
反射是由reflect包來提供支援的,它提供兩種類型來通路接口變量的内容,即Type 和 Value。
reflect包提供了兩個函數來擷取任意對象的Type 和 Value:
- func TypeOf(i interface{}) Type
- func ValueOf(i interface{}) Value
函數 | 作用 |
---|---|
reflect.TypeOf() | 擷取變量的類型資訊,如果為空則傳回nil |
reflect.ValueOf() | 擷取資料的值,如果為空則傳回0 |
示例:
package main
import (
"fmt"
"reflect"
)
func main() {
var name string = "微客鳥窩"
// TypeOf會傳回變量的類型,比如int/float/struct/指針等
reflectType := reflect.TypeOf(name)
// valueOf傳回變量的的值,此處為"微客鳥窩"
reflectValue := reflect.ValueOf(name)
fmt.Println("type: ", reflectType) //type: string
fmt.Println("value: ", reflectValue) //value: 微客鳥窩
}
- 函數 TypeOf 的傳回值 reflect.Type 實際上是一個接口,定義了很多方法來擷取類型相關的資訊:
type Type interface {
// 所有的類型都可以調用下面這些函數
// 此類型的變量對齊後所占用的位元組數
Align() int
// 如果是 struct 的字段,對齊後占用的位元組數
FieldAlign() int
// 傳回類型方法集裡的第 `i` (傳入的參數)個方法
Method(int) Method
// 通過名稱擷取方法
MethodByName(string) (Method, bool)
// 擷取類型方法集裡導出的方法個數
NumMethod() int
// 類型名稱
Name() string
// 傳回類型所在的路徑,如:encoding/base64
PkgPath() string
// 傳回類型的大小,和 unsafe.Sizeof 功能類似
Size() uintptr
// 傳回類型的字元串表示形式
String() string
// 傳回類型的類型值
Kind() Kind
// 類型是否實作了接口 u
Implements(u Type) bool
// 是否可以指派給 u
AssignableTo(u Type) bool
// 是否可以類型轉換成 u
ConvertibleTo(u Type) bool
// 類型是否可以比較
Comparable() bool
// 下面這些函數隻有特定類型可以調用
// 如:Key, Elem 兩個方法就隻能是 Map 類型才能調用
// 類型所占據的位數
Bits() int
// 傳回通道的方向,隻能是 chan 類型調用
ChanDir() ChanDir
// 傳回類型是否是可變參數,隻能是 func 類型調用
// 比如 t 是類型 func(x int, y ... float64)
// 那麼 t.IsVariadic() == true
IsVariadic() bool
// 傳回内部子元素類型,隻能由類型 Array, Chan, Map, Ptr, or Slice 調用
Elem() Type
// 傳回結構體類型的第 i 個字段,隻能是結構體類型調用
// 如果 i 超過了總字段數,就會 panic
Field(i int) StructField
// 傳回嵌套的結構體的字段
FieldByIndex(index []int) StructField
// 通過字段名稱擷取字段
FieldByName(name string) (StructField, bool)
// FieldByNameFunc returns the struct field with a name
// 傳回名稱符合 func 函數的字段
FieldByNameFunc(match func(string) bool) (StructField, bool)
// 擷取函數類型的第 i 個參數的類型
In(i int) Type
// 傳回 map 的 key 類型,隻能由類型 map 調用
Key() Type
// 傳回 Array 的長度,隻能由類型 Array 調用
Len() int
// 傳回類型字段的數量,隻能由類型 Struct 調用
NumField() int
// 傳回函數類型的輸入參數個數
NumIn() int
// 傳回函數類型的傳回值個數
NumOut() int
// 傳回函數類型的第 i 個值的類型
Out(i int) Type
// 傳回類型結構體的相同部分
common() *rtype
// 傳回類型結構體的不同部分
uncommon() *uncommonType
}
- 函數 TypeOf 的傳回值 reflect.Value 是一個結構體類型。Value 結構體定義了很多方法,通過這些方法可以直接操作 Value 字段 ptr 所指向的實際資料:
// 設定切片的 len 字段,如果類型不是切片,就會panic
func (v Value) SetLen(n int)
// 設定切片的 cap 字段
func (v Value) SetCap(n int)
// 設定字典的 kv
func (v Value) SetMapIndex(key, val Value)
// 傳回切片、字元串、數組的索引 i 處的值
func (v Value) Index(i int) Value
// 根據名稱擷取結構體的内部字段值
func (v Value) FieldByName(name string) Value
// ……
struct反射示例:
package main
import (
"fmt"
"reflect"
)
type Address struct {
City string
}
type Person struct {
Name string
Age uint
Address // 匿名字段
}
func (p Person) Hello(){
fmt.Println("我是無塵啊")
}
func main() {
//p := Person{Name:"無塵",Age:18,Address:Address{City:"北京"}} //map指派
p := Person{"無塵",18,Address{"北京"}}
// 擷取目标對象
t := reflect.TypeOf(p)
fmt.Println("t:", t)
// .Name()可以擷取去這個類型的名稱
fmt.Println("類型的名稱:", t.Name())
// 擷取目标對象的值類型
v := reflect.ValueOf(p)
fmt.Println("v:", v)
// .NumField()擷取其包含的字段的總數
for i := 0; i < t.NumField(); i++ {
// 從0開始擷取Person所包含的key
key := t.Field(i)
// interface方法來擷取key所對應的值
value := v.Field(i).Interface()
fmt.Printf("第%d個字段是:%s:%v = %v \n", i+1, key.Name, key.Type, value)
}
// 取出這個City的詳情列印出來
fmt.Printf("%#v\n", t.FieldByIndex([]int{2, 0}))
// .NumMethod()來擷取Person裡的方法
for i:=0;i<t.NumMethod(); i++ {
m := t.Method(i)
fmt.Printf("第%d個方法是:%s:%v\n", i+1, m.Name, m.Type)
}
}
運作結果:
t: main.Person
類型的名稱: Person
v: {無塵 18 {北京}}
第1個字段是:Name:string = 無塵
第2個字段是:Age:uint = 18
第3個字段是:Address:main.Address = {北京}
reflect.StructField{Name:"City", PkgPath:"", Type:(*reflect.rtype)(0x4cfe60), Tag:"", Offset:0x0, Index:[]int{0}, Anonymous:false}
第1個方法是:Hello:func(main.Person)
- 通過反射修改内容
package main
import (
"reflect"
"fmt"
)
type Person struct {
Name string
Age int
}
func main() {
p := &Person{"無塵",18}
v := reflect.ValueOf(p)
// 修改值必須是指針類型
if v.Kind() != reflect.Ptr {
fmt.Println("非指針類型,不能進行修改")
return
}
// 擷取指針所指向的元素
v = v.Elem()
// 擷取目标key的Value的封裝
name := v.FieldByName("Name")
if name.Kind() == reflect.String {
name.SetString("wucs")
}
fmt.Printf("%#v \n", *p)
// 如果是整型的話
test := 666
testV := reflect.ValueOf(&test)
testV.Elem().SetInt(999)
fmt.Println(test)
}
運作結果:
main.Person{Name:"wucs", Age:18}
999
- 通過反射調用方法
package main
import (
"fmt"
"reflect"
)
type Person struct {
Name string
Age int
}
func (p Person) EchoName(name string){
fmt.Println("我的名字是:", name)
}
func main() {
p := Person{Name: "無塵",Age: 18}
v := reflect.ValueOf(p)
// 擷取方法控制權
// 官方解釋:傳回v的名為name的方法的已綁定(到v的持有值的)狀态的函數形式的Value封裝
mv := v.MethodByName("EchoName")
// 拼湊參數
args := []reflect.Value{reflect.ValueOf("wucs")}
// 調用函數
mv.Call(args)
}
運作結果:
我的名字是: wucs
第二部分:Go 的高效并發程式設計執行個體
- 本次給大家介紹的是go程式設計基礎,下一節的并發程式設計後續會推出。感謝大家的觀看。
- 歡迎留言交流,指正
- 我的微信 wucs_dd ,公衆号 《微客鳥窩》,專注于go開發技術分享。