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í)參
- 形參是函數(shù)內(nèi)部的局部變量,實(shí)參的值會拷貝給形參
- 函數(shù)定義時的第一個的大括號不能另起一行
- 形參可以有0個或多個,支持使用可邊長參數(shù)
- 參數(shù)類型相同時可以只寫一次,比如add(a,b int)
- 在函數(shù)內(nèi)部修改形參的值,實(shí)參的值不受影響
- 如果想通過函數(shù)修改實(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ù)時其實(shí)跟普通struct沒什么區(qū)別,都是對struct內(nèi)部的各個字段做一次拷貝傳到函數(shù)內(nèi)部
package main
import "fmt"
// slice作為參數(shù),實(shí)際上是把slice的arrayPointer、len、cap拷貝了一份傳進(jìn)來
func sliceChange(arr []int) {
arr[0] = 1 // 實(shí)際是修改底層數(shù)據(jù)里的首元素
arr = append(arr, 1) // arr的len和cap發(fā)生了變化,不會影響實(shí)參
}
func main() {
arr := []int{8}
sliceChange(arr)
fmt.Println(arr[0]) // 1,數(shù)組元素發(fā)生改變
fmt.Println(len(arr)) // 1,實(shí)際的長度沒有改變
}
關(guān)于函數(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ù)實(shí)際上是slice類型
// other為不定長參數(shù)可傳遞任意多個參數(shù),a是必須傳遞的參數(shù)
func args(a int, other ...int) int {
sum := a
// 直接當(dāng)作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切片,等價(jià)于[]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 // 凡是遞歸,一定要有終止條件,否則會進(jì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)境,閉包復(fù)制的是原對象的指針
package main
import "fmt"
func sub() func() {
i := 10
fmt.Printf("%p\n", &i)
b := func() {
fmt.Printf("i addr %p\n", &i) // 閉包復(fù)制的是原對象的指針
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不是一個實(shí)體了
tmp2 := add(100)
fmt.Println(tmp2(1), tmp2(2))
}

5. 延遲調(diào)用defer
- defer用于注冊一個延遲調(diào)用(在函數(shù)返回之前調(diào)用)
- defer典型的應(yīng)用場景是釋放資源,比如關(guān)閉文件句柄,釋放數(shù)據(jù)庫連接等
- 如果同一個函數(shù)里有多個defer,則后注冊的先執(zhí)行,相當(dāng)于是一個棧
- defer后可以跟一個func,func內(nèi)部如果發(fā)生panic,會把panic暫時擱置,當(dāng)把其他defer執(zhí)行完之后再來執(zhí)行這個
- defer后不是跟func,而直接跟一條執(zhí)行語句,則相關(guān)變量在注冊defer時被拷貝或計(jì)算
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í)行語句,則相關(guān)變量在注冊defer時被拷貝或計(jì)算
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要實(shí)現(xiàn)Error()方法
// 自定義error
type PathError struct {
path string
op string
createTime string
message string
}
// error接口要求實(shí)現(xiàn)Error() string方法
func (err PathError) Error() string {
return err.createTime + ": " + err.op + " " + err.path + " " + err.message
}
何時會發(fā)生panic:
- 運(yùn)行時錯誤會導(dǎo)致panic,比如數(shù)組越界、除0
- 程序主動調(diào)用panic(error)
panic會執(zhí)行什么:
- 逆序執(zhí)行當(dāng)前goroutine的defer鏈(recover從這里介入)
- 打印錯誤信息和調(diào)用堆棧
- 調(diào)用exit(2)結(jié)束整個進(jìn)程
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結(jié)尾
type Transporter interface {
// 接口里面只定義方法,不定義變量
move(src string, dest string) (int, error) // 方法名 (參數(shù)列表) 返回值列表
whistle(int) int // 參數(shù)列表和返回值列表里的變量名可以省略
}
只要結(jié)構(gòu)體擁有接口里聲明的所有方法,就稱該結(jié)構(gòu)體“實(shí)現(xiàn)了接口”,一個struct可以同時實(shí)現(xiàn)多個接口
// 定義結(jié)構(gòu)體時無需要顯式聲明它要實(shí)現(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í)數(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實(shí)現(xiàn)了Transporter接口
var ship Shiper // Shiper實(shí)現(xiàn)了Transporter接口
transport("北京", "天津", car)
transport("北京", "天津", ship)
3. 接口的賦值
// 方法接收者是值
func (car Car) whistle(n int) int {
}
// 方法接收者用指針,則實(shí)現(xiàn)接口的是指針類型
func (ship *Shiper) whistle(n int) int {
}
car := Car{}
ship := Shiper{}
var transporter Transporter
transporter = car
transporter = &car // 值實(shí)現(xiàn)的方法,默認(rèn)指針同樣也實(shí)現(xiàn)了
transporter = &ship // 但指針實(shí)現(xiàn)的方法,值是沒有實(shí)現(xiàn)的
4. 接口嵌入
type Transporter interface {
whistle(int) int
}
type Steamer interface {
Transporter // 接口嵌入,相當(dāng)于Transporter接口定義的行為集合是Steamer的子集
displacement() int
}
5. 空接口
空接口類型用interface{}表示,注意有{}
var i interface{<!--{cke_protected}{C}%3C!%2D%2D%20%2D%2D%3E-->} 空接口沒有定義任何方法,因此任意類型都實(shí)現(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類型")
}
當(dāng)要判斷的類型比較多時,就需要寫很多if-else,更好的方法是使用switch i.(type),這也是標(biāo)準(zhǔn)的寫法
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)一設(shè)置每路最多召回10個商品
for _, product := range products {
RecallMap[product.Id] = product // 把多路召回的結(jié)果放到map里,按Id進(jìn)行排重
}
}
// 把map轉(zhuǎn)成slice
RecallSlice := make([]*common.Product, 0, len(RecallMap))
for _, product := range RecallMap {
RecallSlice = append(RecallSlice, product)
}
SortedResult := rec.Sorter.Sort(RecallSlice) // 對召回的結(jié)果進(jìn)行排序
// 順序執(zhí)行多種過濾規(guī)則
FilteredResult := SortedResult
for _, filter := range rec.Filters {
FilteredResult = filter.Filter(FilteredResult)
}
return FilteredResult
}
面向接口編程,在框架層面全是接口。具體的實(shí)現(xiàn)由不同的開發(fā)者去完成,每種實(shí)現(xiàn)單獨(dú)放到一個go文件里,大家的代碼互不干擾。通過配置選擇采用哪種實(shí)現(xiàn),也方便進(jìn)行效果對比
到此這篇關(guān)于GoLang函數(shù)與面向接口編程全面分析講解的文章就介紹到這了,更多相關(guān)GoLang函數(shù)與面向接口內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Go?Wails開發(fā)桌面應(yīng)用使用示例探索
這篇文章主要為大家介紹了Go?Wails的使用示例探索,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-12-12
Golang?channel關(guān)閉后是否可以讀取剩余的數(shù)據(jù)詳解
這篇文章主要介紹了Golang?channel關(guān)閉后是否可以讀取剩余的數(shù)據(jù),文章通過一個測試?yán)咏o大家詳細(xì)的介紹了是否可以讀取剩余的數(shù)據(jù),需要的朋友可以參考下2023-09-09
go string to int 字符串與整數(shù)型的互換方式
這篇文章主要介紹了go string to int 字符串與整數(shù)型的互換方式,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-07-07

