GoLang函數(shù)與面向接口編程全面分析講解
一、函數(shù)
1. 函數(shù)的基本形式
// 函數(shù)定義:a,b是形參 func add(a int, b int) { a = a + b } var x, y int = 3, 6 add(x, y) // 函數(shù)調(diào)用:x,y是實參
- 形參是函數(shù)內(nèi)部的局部變量,實參的值會拷貝給形參
- 函數(shù)定義時的第一個的大括號不能另起一行
- 形參可以有0個或多個,支持使用可邊長參數(shù)
- 參數(shù)類型相同時可以只寫一次,比如add(a,b int)
- 在函數(shù)內(nèi)部修改形參的值,實參的值不受影響
- 如果想通過函數(shù)修改實參,就需要傳遞指針類型
func change(a, b *int) { *a = *a + *b *b = 888 } var x, y int = 3, 6 change(&x, &y)
slice、map、channel都是引用類型,它們作為函數(shù)參數(shù)時其實跟普通struct沒什么區(qū)別,都是對struct內(nèi)部的各個字段做一次拷貝傳到函數(shù)內(nèi)部
package main import "fmt" // slice作為參數(shù),實際上是把slice的arrayPointer、len、cap拷貝了一份傳進來 func sliceChange(arr []int) { arr[0] = 1 // 實際是修改底層數(shù)據(jù)里的首元素 arr = append(arr, 1) // arr的len和cap發(fā)生了變化,不會影響實參 } func main() { arr := []int{8} sliceChange(arr) fmt.Println(arr[0]) // 1,數(shù)組元素發(fā)生改變 fmt.Println(len(arr)) // 1,實際的長度沒有改變 }
關于函數(shù)返回值
- 可以返回0個或多個參數(shù)
- 可以在func行直接聲明要返回的變量
- return后面的語句不會執(zhí)行
- 無返回參數(shù)時return可以不寫
// 返回變量c已經(jīng)聲明好了,在函數(shù)中可以直接使用 func returnf(a, b int) (c int) { a = a + b c = a // 直接使用c return // 由于函數(shù)要求有返回值,即使給c賦過值了,也需要顯式寫return }
不定長參數(shù)實際上是slice類型
// other為不定長參數(shù)可傳遞任意多個參數(shù),a是必須傳遞的參數(shù) func args(a int, other ...int) int { sum := a // 直接當作slice來使用 for _, ele := range other { sum += ele } fmt.Printf("len %d cap %d\n", len(other), cap(other)) return sum } args(1) args(1,2,3,4)
append函數(shù)接收的就是不定長參數(shù)
arr = append(arr, 1, 2, 3) arr = append(arr, 7) arr = append(arr) slice := append([]byte("hello "), "world"...) // ...自動把"world"轉(zhuǎn)成byte切片,等價于[]byte("world")... slice2 := append([]rune("hello "), []rune("world")...) // 需要顯式把"world"轉(zhuǎn)成rune切片
在很多場景下string都隱式的轉(zhuǎn)換成了byte切片,而非rune切片,比如"a中"[1]
獲取到的值為228而非"中"
2. 遞歸函數(shù)
最經(jīng)典的斐波那契數(shù)列的遞歸求法
func fibonacci(n int) int { if n == 0 || n == 1 { return n // 凡是遞歸,一定要有終止條件,否則會進入無限循環(huán) } return fibonacci(n-1) + fibonacci(n-2) // 遞歸調(diào)用自身 }
3. 匿名函數(shù)
函數(shù)也是一種數(shù)據(jù)類型
func functionArg1(f func(a, b int) int, b int) int { // f參數(shù)是一種函數(shù)類型 a := 2 * b return f(a, b) } type foo func(a, b int) int // foo是一種函數(shù)類型 func functionArg2(f foo, b int) int { // type重命名之后,參數(shù)類型看上去簡潔多了 a := 2 * b return f(a, b) } type User struct { Name string bye foo // bye的類型是foo,也就是是函數(shù)類型 hello func(name string) string // 使用匿名函數(shù)來聲明struct字段的類型為函數(shù)類型 } ch := make(chan func(string) string, 10) // 使用匿名函數(shù)向管道中添加元素 ch <- func(name string) string { return "hello " + name }
4. 閉包
閉包(Closure)是引用了自由變量的函數(shù),自由變量將和函數(shù)一同存在,即使已經(jīng)離開了創(chuàng)造它的環(huán)境,閉包復制的是原對象的指針
package main import "fmt" func sub() func() { i := 10 fmt.Printf("%p\n", &i) b := func() { fmt.Printf("i addr %p\n", &i) // 閉包復制的是原對象的指針 i-- // b函數(shù)內(nèi)部引用了變量i fmt.Println(i) } return b // 返回了b函數(shù),變量i和函數(shù)b將一起存在,即使已經(jīng)離開函數(shù)sub() } // 外部引用函數(shù)參數(shù)局部變量 func add(base int) func(int) int { return func(i int) int { fmt.Printf("base addr %p\n", &base) base += i return base } } func main() { b := sub() b() b() fmt.Println() tmp1 := add(10) fmt.Println(tmp1(1), tmp1(2)) // 此時tmp1和tmp2不是一個實體了 tmp2 := add(100) fmt.Println(tmp2(1), tmp2(2)) }
5. 延遲調(diào)用defer
- defer用于注冊一個延遲調(diào)用(在函數(shù)返回之前調(diào)用)
- defer典型的應用場景是釋放資源,比如關閉文件句柄,釋放數(shù)據(jù)庫連接等
- 如果同一個函數(shù)里有多個defer,則后注冊的先執(zhí)行,相當于是一個棧
- defer后可以跟一個func,func內(nèi)部如果發(fā)生panic,會把panic暫時擱置,當把其他defer執(zhí)行完之后再來執(zhí)行這個
- defer后不是跟func,而直接跟一條執(zhí)行語句,則相關變量在注冊defer時被拷貝或計算
func basic() { fmt.Println("A") defer fmt.Println(1) fmt.Println("B") // 如果同一個函數(shù)里有多個defer,則后注冊的先執(zhí)行 defer fmt.Println(2) fmt.Println("C") }
func deferExecTime() (i int) { i = 9 // defer后可以跟一個func defer func() { fmt.Printf("first i=%d\n", i) // 打印5,而非9,充分理解“defer在函數(shù)返回前執(zhí)行”的含義,不是在“return語句前執(zhí)行defer” }() defer func(i int) { fmt.Printf("second i=%d\n", i) // 打印9 }(i) defer fmt.Printf("third i=%d\n", i) // 打印9,defer后不是跟func,而直接跟一條執(zhí)行語句,則相關變量在注冊defer時被拷貝或計算 return 5 }
6. 異常處理
go語言沒有try catch,它提倡直接返回error
func divide(a, b int) (int, error) { if b == 0 { return -1, errors.New("divide by zero") } return a / b, nil } // 函數(shù)調(diào)用方判斷error是否為nil,不為nil則表示發(fā)生了錯誤 if res, err := divide(3, 0); err != nil { fmt.Println(err.Error()) }
Go語言定義了error這個接口,自定義的error要實現(xiàn)Error()方法
// 自定義error type PathError struct { path string op string createTime string message string } // error接口要求實現(xiàn)Error() string方法 func (err PathError) Error() string { return err.createTime + ": " + err.op + " " + err.path + " " + err.message }
何時會發(fā)生panic:
- 運行時錯誤會導致panic,比如數(shù)組越界、除0
- 程序主動調(diào)用panic(error)
panic會執(zhí)行什么:
- 逆序執(zhí)行當前goroutine的defer鏈(recover從這里介入)
- 打印錯誤信息和調(diào)用堆棧
- 調(diào)用exit(2)結束整個進程
func soo() { fmt.Println("enter soo") // 去掉這個defer試試,看看panic的流程,把這個defer放到soo函數(shù)末尾試試 defer func() { // recover必須在defer中才能生效 if err := recover(); err != nil { fmt.Printf("soo panic:%s\n", err) } }() fmt.Println("regist recover") defer fmt.Println("hello") defer func() { n := 0 _ = 3 / n // 除0異常,發(fā)生panic,下一行的defer沒有注冊成功 defer fmt.Println("how are you") }() }
二、面向接口編程
1. 接口的基本概念
接口是一組行為規(guī)范的集合
// 定義接口,通常接口名以er結尾 type Transporter interface { // 接口里面只定義方法,不定義變量 move(src string, dest string) (int, error) // 方法名 (參數(shù)列表) 返回值列表 whistle(int) int // 參數(shù)列表和返回值列表里的變量名可以省略 }
只要結構體擁有接口里聲明的所有方法,就稱該結構體“實現(xiàn)了接口”,一個struct可以同時實現(xiàn)多個接口
// 定義結構體時無需要顯式聲明它要實現(xiàn)什么接口 type Car struct { price int } func (car Car) move(src string, dest string) (int, error) { return car.price, nil } func (car Car) whistle(n int) int { return n }
接口值有兩部分組成, 一個指向該接口的具體類型的指針和另外一個指向該具體類型真實數(shù)據(jù)的指針
car := Car{"寶馬", 100} var transporter Transporter transporter = car
2. 接口的使用
func transport(src, dest string, transporter Transporter) error { _,err := transporter.move(src, dest) return err } var car Car // Car實現(xiàn)了Transporter接口 var ship Shiper // Shiper實現(xiàn)了Transporter接口 transport("北京", "天津", car) transport("北京", "天津", ship)
3. 接口的賦值
// 方法接收者是值 func (car Car) whistle(n int) int { } // 方法接收者用指針,則實現(xiàn)接口的是指針類型 func (ship *Shiper) whistle(n int) int { } car := Car{} ship := Shiper{} var transporter Transporter transporter = car transporter = &car // 值實現(xiàn)的方法,默認指針同樣也實現(xiàn)了 transporter = &ship // 但指針實現(xiàn)的方法,值是沒有實現(xiàn)的
4. 接口嵌入
type Transporter interface { whistle(int) int } type Steamer interface { Transporter // 接口嵌入,相當于Transporter接口定義的行為集合是Steamer的子集 displacement() int }
5. 空接口
空接口類型用interface{}表示,注意有{}
var i interface{<!--{cke_protected}{C}%3C!%2D%2D%20%2D%2D%3E-->}
空接口沒有定義任何方法,因此任意類型都實現(xiàn)了空接口
var a int = 5 i = a
func square(x interface{<!--{cke_protected}{C}%3C!%2D%2D%20%2D%2D%3E-->}){<!--{cke_protected}{C}%3C!%2D%2D%20%2D%2D%3E-->} // 該函數(shù)可以接收任意數(shù)據(jù)類型
注意:slice的元素、map的key和value都可以是空接口類型,map中的key可以是任意能夠用==操作符比較的類型,不能是函數(shù)、map、切片,以及包含上述3中類型成員變量的的struct,map的value可以是任意類型
6. 類型斷言
// 若斷言成功,則ok為true,v是具體的類型 if v, ok := i.(int); ok { fmt.Printf("i是int類型,其值為%d\n", v) } else { fmt.Println("i不是int類型") }
當要判斷的類型比較多時,就需要寫很多if-else,更好的方法是使用switch i.(type),這也是標準的寫法
switch v := i.(type) { // 隱式地在每個case中聲明了一個變量v case int: // v已被轉(zhuǎn)為int類型 fmt.Printf("ele is int, value is %d\n", v) // 在 Type Switch 語句的 case 子句中不能使用fallthrough case float64: // v已被轉(zhuǎn)為float64類型 fmt.Printf("ele is float64, value is %f\n", v) case int8, int32, byte: // 如果case后面跟多種type,則v還是interface{}類型 fmt.Printf("ele is %T, value is %d\n", v, v) }
7. 面向接口編程
電商推薦流程
為每一個步驟定義一個接口
type Recaller interface { Recall(n int) []*common.Product // 生成一批推薦候選集 } type Sorter interface { Sort([]*common.Product) []*common.Product // 傳入一批商品,返回排序之后的商品 } type Filter interface { Filter([]*common.Product) []*common.Product // 傳入一批商品,返回過濾之后的商品 } type Recommender struct { Recallers []recall.Recaller Sorter sort.Sorter Filters []filter.Filter }
使用純接口編寫推薦主流程
func (rec *Recommender) Rec() []*common.Product { RecallMap := make(map[int]*common.Product, 100) // 順序執(zhí)行多路召回 for _, recaller := range rec.Recallers { products := recaller.Recall(10) // 統(tǒng)一設置每路最多召回10個商品 for _, product := range products { RecallMap[product.Id] = product // 把多路召回的結果放到map里,按Id進行排重 } } // 把map轉(zhuǎn)成slice RecallSlice := make([]*common.Product, 0, len(RecallMap)) for _, product := range RecallMap { RecallSlice = append(RecallSlice, product) } SortedResult := rec.Sorter.Sort(RecallSlice) // 對召回的結果進行排序 // 順序執(zhí)行多種過濾規(guī)則 FilteredResult := SortedResult for _, filter := range rec.Filters { FilteredResult = filter.Filter(FilteredResult) } return FilteredResult }
面向接口編程,在框架層面全是接口。具體的實現(xiàn)由不同的開發(fā)者去完成,每種實現(xiàn)單獨放到一個go文件里,大家的代碼互不干擾。通過配置選擇采用哪種實現(xiàn),也方便進行效果對比
到此這篇關于GoLang函數(shù)與面向接口編程全面分析講解的文章就介紹到這了,更多相關GoLang函數(shù)與面向接口內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Golang?channel關閉后是否可以讀取剩余的數(shù)據(jù)詳解
這篇文章主要介紹了Golang?channel關閉后是否可以讀取剩余的數(shù)據(jù),文章通過一個測試例子給大家詳細的介紹了是否可以讀取剩余的數(shù)據(jù),需要的朋友可以參考下2023-09-09go string to int 字符串與整數(shù)型的互換方式
這篇文章主要介紹了go string to int 字符串與整數(shù)型的互換方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-07-07