Go基礎3:函數、結構體、方法、介面
1. 函數
Go語言的函數屬於「一等公民」(first-class),也就是說:
- 函數本身可以作為值進行傳遞。
- 支援匿名函數和閉包(closure)。
- 函數可以滿足介面。
1.1 函數返回值
同一種類型返回值
func typedTwoValues() (int, int) {
return 1, 2
}
a, b := typedTwoValues()
fmt.Println(a, b)
帶變數名的返回值
func named Ret Values() (a, b int) {
a = 1
b = 2
return
}
函數使用命名返回值時,可以在return中不填寫返回值列表,如果填寫也是可行的
函數中的參數傳遞
Go語言中傳入和返回參數在調用和返回時都使用值傳遞,這裡需要注意的是指針、切片和map等引用型對象指向的內容在參數傳遞中不會發生複製,而是將指針進行複製,類似於創建一次引用。
函數變數
在Go語言中,函數也是一種類型,可以和其他類型一樣被保存在變數中
func fire() {
fmt.Println("fire")
}
func main() {
var f func() //將變數f聲明為func()類型,此時f就被俗稱為「回調函數」。此時f的值為nil。
f = fire
f()
}
1.2 匿名函數——沒有函數名字的函數
Go語言支援匿名函數,即在需要使用函數時,再定義函數,匿名函數沒有函數名,只有函數體,函數可以被作為一種類型被賦值給函數類型的變數,匿名函數也往往以變數方式被傳遞。
在定義時調用匿名函數
匿名函數可以在聲明後調用,例如:
func(data int) {
fmt.Println("hello", data)
}(100)
將匿名函數賦值給變數
匿名函數體可以被賦值,例如:
// 將匿名函數體保存到f()中
f := func(data int) {
fmt.Println("hello", data)
}
// 使用f()調用
f(100)
匿名函數用作回調函數
使用時再定義匿名函數,不使用先在被調用函數裡面進行聲明,這就是回調精髓
// 遍歷切片的每個元素,通過給定函數進行元素訪問
func visit(list []int, f func(int)) {
for _, v := range list {
f(v)
}
}
func main() {
// 使用匿名函數列印切片內容
visit([]int{1, 2, 3, 4}, func(v int) {
fmt.Println(v)
})
}
可變參數——參數數量不固定的函數形式
1.所有參數都是可變參數:fmt.Println
func Println(a ...interface{}) (n int, err error) {
return Fprintln(os.Stdout, a...)
}
fmt.Println在使用時,傳入的值類型不受限制,例如:
fmt.Println(5, "hello", &struct{ a int }{1}, true)
當可變參數為
interface{}
類型時,可以傳入任何類型的值
2.部分參數是可變參數:fmt.Printf
fmt.Printf的第一個參數為參數列表,後面的參數是可變參數:
func Printf(format string, a ...interface{}) (n int, err error) {
return Fprintf(os.Stdout, format, a...)
}
------------------------------------------------------
fmt.Printf("pure string\n")
fmt.Printf("value: %v %f\n", true, math.Pi)
1.3 閉包
閉包可以理解成定義在函數內部的一個函數。本質上,閉包是函數內部和函數外部連接起來的橋樑。
簡單來說,閉包=函數+引用環境
func main() {
var f = add()
fmt.Printf("f(10): %v\n", f(10))
fmt.Printf("f(20): %v\n", f(20))
// f(10): 10
// f(20): 30
}
func add() func(int) int {
var x int
return func(y int) int {
x += y
return x
}
}
1.4 defer語句
defer語句將其後面跟隨的語句進行延遲處理,被defer的語句按先進後出的方式執行(最先defer的語句最後執行,後被defer的語句先執行)。
特性:
- 關鍵字defer用於註冊延遲調用
- 直到調用return之前才執行(故可用來作資源清理)
- 多個defer語句,FILO方式執行
- defer中的變數,在defer聲明時就定義了
用途:
- 關閉文件句柄
- 鎖資源釋放
- 資料庫連接釋放
處理運行時發生的錯誤
Go語言的錯誤處理思想及設計包含以下特徵:
● 一個可能造成錯誤的函數,需要返回值中返回一個錯誤介面(error)。如果調用是成功的,錯誤介面將返回nil,否則返回錯誤。
● 在函數調用後需要檢查錯誤,如果發生錯誤,進行必要的錯誤處理。
錯誤介面的定義格式
error是Go系統聲明的介面類型,程式碼如下:
type error interface {
Error() string // 返回錯誤的具體描述.
}
所有符合Error() string格式的介面都能實現錯誤介面。
定義一個錯誤
在Go語言中,使用errors包進行錯誤的定義,格式如下:
var err = errors.New("this is an error")
錯誤字元串由於相對固定,一般在包作用域聲明,應盡量減少在使用時直接使用errors.New返回。
宕機(panic)——程式終止運行
1 手動觸發宕機
Go語言可以在程式中手動觸發宕機,讓程式崩潰,這樣開發者可以及時地發現錯誤,同時減少可能的損失。
Go語言程式在宕機時,會將堆棧和goroutine資訊輸出到控制台,所以宕機也可以方便地知曉發生錯誤的位置。
package main
func main() {
panic(“crash”)
}
panic()的參數可以是任意類型,
當panic()觸發的宕機發生時,panic()後面的程式碼將不會被運行,但是在panic()函數前面已經運行過的defer語句依然會在宕機發生時發生作用,
1.5 宕機恢復(recover)——防止程式崩潰
無論是程式碼運行錯誤由Runtime層拋出的panic崩潰,還是主動觸發的panic崩潰,都可以配合defer和recover實現錯誤捕捉和恢復,讓程式碼在發生崩潰後允許繼續運行。
Go沒有異常系統,其使用panic觸發宕機類似於其他語言的拋出異常,那麼recover的宕機恢復機制就對應try/catch機制。
panic和recover的關係:
● 有panic沒recover,程式宕機。
● 有panic也有recover捕獲,程式不會宕機。執行完對應的defer後,從宕機點退出當前函數後繼續執行。
提示:雖然panic/recover能模擬其他語言的異常機制,但並不建議代表編寫普通函數也經常性使用這種特性。
2. 結構體
結構體成員是由一系列的成員變數構成,這些成員變數也被稱為「欄位」。欄位有以下特性:
● 欄位擁有自己的類型和值。
● 欄位名必須唯一。
● 欄位的類型也可以是結構體,甚至是欄位所在結構體的類型。
Go語言中沒有「類」的概念,也不支援「類」的繼承等面向對象的概念。
Go語言的結構體與「類」都是複合結構體,但Go語言中結構體的內嵌配合介面比面向對象具有更高的擴展性和靈活性。
Go語言不僅認為結構體能擁有方法,且每種自定義類型也可以擁有自己的方法。
2.1 定義與給結構體賦值
基本形式:
type Point struct {
X int
Y int
}
var p Point
p.X = 10
p.Y = 20
結構體的定義只是一種記憶體布局的描述,只有當結構體實例化時,才會真正地分配記憶體
創建指針類型的結構體:
type Player struct {
name string
age int
}
p = new(Player)
p.name = "james"
p.age = 40
取結構體的地址實例化:
//使用結構體定義一個命令行指令(Command),指令中包含名稱、變數關聯和注釋等
type Command struct {
name string
Var *int
comment string
}
var version int = 1
cmd := &Command{}
cmd.name = "version"
cmd.Var = &version
cmd.comment = "show version"
使用鍵值對填充結構體:
type People struct {
name string
child *People
}
relation := &People{
name: "爺爺"
child: &People{
name: "爸爸"
child: &People{
name: "我"
},
}
}
3. 方法
Go語言中的方法(Method)是一種作用於特定類型變數的函數。這種特定類型變數叫做接收器(Receiver)。
如果將特定類型理解為結構體或「類」時,接收器的概念就類似於其他語言中的this
或者self
。
結構體方法
創建一個背包Bag
結構體為其定義把物品放入背包的方法insert
:
type Bag struct {
items[] int
}
func (b *Bag) insert(itemid int) {
b.items = append(b.items, itemid)
}
func main() {
b := new(Bag)
b.insert(1001)
}
(b*Bag)
表示接收器,即Insert
作用的對象實例。每個方法只能有一個接收器。
接收器
接收器是方法作用的目標
接收器根據接收器的類型可分:
- 指針接收器
- 非指針接收器
- 兩種接收器在使用時會產生不同的效果。根據效果的不同,兩種接收器會被用於不同性能和功能要求的程式碼中。
指針接收器
由於指針的特性,調用方法時,修改接收器指針的任意成員變數,在方法結束後,修改都是有效的。
// 定義屬性結構
type Property struct {
value int
}
// 設置屬性值方法
func (p *Property) setVal(val int) {
p.value = val
}
// 獲取屬性值方法
func (p *Property) getVal() int {
return p.value
}
func main() {
p := new(Property)
p.value = 123
fmt.Println(p.getVal())
p.setVal(666)
fmt.Println(p.getVal())
}
非指針類型接收器
當方法作用於非指針接收器時,Go語言會在程式碼運行時將接收器的值複製一份。在非指針接收器的方法中可以獲取接收器的成員值,但修改後無效。
type Point struct {
x, y int
}
func (p Point) add(other Point) Point {
return Point{p.x + other.x, p.y + other.y}
}
func main() {
// 初始化點
p1 := Point{1, 1}
p2 := Point{2, 2}
res := p1.add(p2)
fmt.Println(res)
p3 := Point{3, 3}
p4 := p1.add(p2).add(p3)
fmt.Println(p4)
}
指針接收器和非指針接收器的使用:
指針和非指針接收器的使用在電腦中,小對象由於值複製時的速度較快,所以適合使用非指針接收器。大對象因為複製性能較低,適合使用指針接收器,在接收器和參數間傳遞時不進行複製,只是傳遞指針。
4. 介面
介面是雙方約定的一種合作協議。介面實現者不需要關心介面會被怎樣使用,調用者也不需要關心介面的實現細節。介面是一種類型,也是一種抽象結構,不會暴露所含數據的格式、類型及結構。
聲明介面
type 介面類型名 interface {
方法1(參數列表) 返回值
...
}
Go語言的介面在命名時,一般會在單詞後面添加er,如有寫操作的介面叫Writer,有字元串功能的介面叫Stringer,有關閉功能的介面叫Closer等
方法名:當方法名首字母是大寫時,且這個介面類型名首字母也是大寫時,這個方法可以被介面所在的包(package)之外的程式碼訪問。
io包中提供的Writer介面:
type Writer interface {
Write(p []type) (n int, err error)
}
實現介面
實現介面的條件:
- 介面的方法與實現介面的類型方法格式一致
- 介面中所有方法均被實現
例:為了抽象數據寫入的過程,定義Data Writer介面來描述數據寫入需要實現的方法。
// 定義一個數據寫入器介面
type DataWriter interface {
WriteData(data interface{}) error
}
// 定義文件結構,用於實現DataWriter
type file struct {
}
// 實現DataWriter介面的方法
func (d *file) WriteData(data interface{}) error {
// 模擬寫入數據
fmt.Println("Write Data:", data)
return nil
}
func main() {
// 實例化file
f := new(file)
// 聲明一個DataWriter介面
var writer DataWriter
// 將介面賦值,也就是*file
writer = f
writer.WriteData("one line data")
}
Go語言的介面實現是隱式的,無須讓實現介面的類型寫出實現了哪些介面。這個設計被稱為非侵入式設計。