首頁 > 軟體

GoLang函數與面向介面程式設計全面分析講解

2023-11-01 18:00:27

一、函數

1. 函數的基本形式

// 函數定義:a,b是形參
func add(a int, b int) { 
	a = a + b 
}
var x, y int = 3, 6
add(x, y) // 函數呼叫:x,y是實參
  • 形參是函數內部的區域性變數,實參的值會拷貝給形參
  • 函數定義時的第一個的大括號不能另起一行
  • 形參可以有0個或多個,支援使用可邊長引數
  • 引數型別相同時可以只寫一次,比如add(a,b int)
  • 在函數內部修改形參的值,實參的值不受影響
  • 如果想通過函數修改實參,就需要傳遞指標型別
func change(a, b *int) { 
    *a = *a + *b
    *b = 888
}
var x, y int = 3, 6
change(&x, &y)

slice、map、channel都是參照型別,它們作為函數引數時其實跟普通struct沒什麼區別,都是對struct內部的各個欄位做一次拷貝傳到函數內部

package main
import "fmt"
// slice作為引數,實際上是把slice的arrayPointer、len、cap拷貝了一份傳進來
func sliceChange(arr []int) { 
	arr[0] = 1 // 實際是修改底層資料裡的首元素
	arr = append(arr, 1) // arr的len和cap發生了變化,不會影響實參
}
func main() {
	arr := []int{8}
	sliceChange(arr)
	fmt.Println(arr[0])   // 1,陣列元素髮生改變
	fmt.Println(len(arr)) // 1,實際的長度沒有改變
}

關於函數返回值

  • 可以返回0個或多個引數
  • 可以在func行直接宣告要返回的變數
  • return後面的語句不會執行
  • 無返回引數時return可以不寫
// 返回變數c已經宣告好了,在函數中可以直接使用
func returnf(a, b int) (c int) {
    a = a + b
    c = a // 直接使用c
    return // 由於函數要求有返回值,即使給c賦過值了,也需要顯式寫return
}

不定長引數實際上是slice型別

// other為不定長引數可傳遞任意多個引數,a是必須傳遞的引數
func args(a int, other ...int) int { 
    sum := a
    // 直接當作slice來使用
    for _, ele := range other {
        sum += ele
    }
    fmt.Printf("len %d cap %dn", len(other), cap(other))
    return sum
}
args(1)
args(1,2,3,4)

append函數接收的就是不定長引數

arr = append(arr, 1, 2, 3)
arr = append(arr, 7)
arr = append(arr)
slice := append([]byte("hello "), "world"...) // ...自動把"world"轉成byte切片,等價於[]byte("world")...
slice2 := append([]rune("hello "), []rune("world")...) // 需要顯式把"world"轉成rune切片

在很多場景下string都隱式的轉換成了byte切片,而非rune切片,比如"a中"[1]獲取到的值為228而非"中"

2. 遞迴函數

最經典的斐波那契數列的遞迴求法

func fibonacci(n int) int {
    if n == 0 || n == 1 {
        return n // 凡是遞迴,一定要有終止條件,否則會進入無限迴圈
    }
    return fibonacci(n-1) + fibonacci(n-2) // 遞迴呼叫自身
}

3. 匿名函數

函數也是一種資料型別

func functionArg1(f func(a, b int) int, b int) int { // f引數是一種函數型別
	a := 2 * b
	return f(a, b)
}
type foo func(a, b int) int // foo是一種函數型別
func functionArg2(f foo, b int) int { // type重新命名之後,引數型別看上去簡潔多了
    a := 2 * b
    return f(a, b)
}
type User struct {
    Name string
    bye foo // bye的型別是foo,也就是是函數型別
    hello func(name string) string // 使用匿名函數來宣告struct欄位的型別為函數型別
}
ch := make(chan func(string) string, 10)
// 使用匿名函數向管道中新增元素
ch <- func(name string) string {
	return "hello " + name
}

4. 閉包

閉包(Closure)是參照了自由變數的函數,自由變數將和函數一同存在,即使已經離開了創造它的環境,閉包複製的是原物件的指標

package main
import "fmt"
func sub() func() {
	i := 10
	fmt.Printf("%pn", &i)
	b := func() {
		fmt.Printf("i addr %pn", &i) // 閉包複製的是原物件的指標
		i-- // b函數內部參照了變數i
		fmt.Println(i)
	}
	return b // 返回了b函數,變數i和函數b將一起存在,即使已經離開函數sub()
}
// 外部參照函數引數區域性變數
func add(base int) func(int) int {
	return func(i int) int {
		fmt.Printf("base addr %pn", &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. 延遲呼叫defer

  • defer用於註冊一個延遲呼叫(在函數返回之前呼叫)
  • defer典型的應用場景是釋放資源,比如關閉檔案控制程式碼,釋放資料庫連線等
  • 如果同一個函數裡有多個defer,則後註冊的先執行,相當於是一個棧
  • defer後可以跟一個func,func內部如果發生panic,會把panic暫時擱置,當把其他defer執行完之後再來執行這個
  • defer後不是跟func,而直接跟一條執行語句,則相關變數在註冊defer時被拷貝或計算
func basic() {
    fmt.Println("A")
    defer fmt.Println(1) fmt.Println("B")
    // 如果同一個函數裡有多個defer,則後註冊的先執行
    defer fmt.Println(2)
    fmt.Println("C")
}
func deferExecTime() (i int) {
	i = 9
	// defer後可以跟一個func
	defer func() {
		fmt.Printf("first i=%dn", i) // 列印5,而非9,充分理解「defer在函數返回前執行」的含義,不是在「return語句前執行defer」
	}()
	defer func(i int) {
		fmt.Printf("second i=%dn", i) // 列印9
	}(i)
	defer fmt.Printf("third i=%dn", i) // 列印9,defer後不是跟func,而直接跟一條執行語句,則相關變數在註冊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
}
// 函數呼叫方判斷error是否為nil,不為nil則表示發生了錯誤
if res, err := divide(3, 0); err != nil {
    fmt.Println(err.Error())
}

Go語言定義了error這個介面,自定義的error要實現Error()方法

// 自定義error
type PathError struct {
    path string
    op string
    createTime string
    message string
}
// error介面要求實現Error() string方法
func (err PathError) Error() string {
	return err.createTime + ": " + err.op + " " + err.path + " " + err.message
}

何時會發生panic:

  • 執行時錯誤會導致panic,比如陣列越界、除0
  • 程式主動呼叫panic(error)

panic會執行什麼:

  • 逆序執行當前goroutine的defer鏈(recover從這裡介入)
  • 列印錯誤資訊和呼叫堆疊
  • 呼叫exit(2)結束整個程序
func soo() {
	fmt.Println("enter soo")
	// 去掉這個defer試試,看看panic的流程,把這個defer放到soo函數末尾試試
	defer func() {
		// recover必須在defer中才能生效
		if err := recover(); err != nil {
			fmt.Printf("soo panic:%sn", err)
		}
	}()
	fmt.Println("regist recover")
	defer fmt.Println("hello")
	defer func() {
		n := 0
		_ = 3 / n // 除0異常,發生panic,下一行的defer沒有註冊成功
		defer fmt.Println("how are you")
	}()
}

二、面向介面程式設計

1. 介面的基本概念

介面是一組行為規範的集合

// 定義介面,通常介面名以er結尾
type Transporter interface {
    // 介面裡面只定義方法,不定義變數
    move(src string, dest string) (int, error) // 方法名 (參數列) 返回值列表
    whistle(int) int // 參數列和返回值列表裡的變數名可以省略
}

只要結構體擁有介面裡宣告的所有方法,就稱該結構體“實現了介面”,一個struct可以同時實現多個介面

// 定義結構體時無需要顯式宣告它要實現什麼介面
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
}

介面值有兩部分組成, 一個指向該介面的具體型別的指標和另外一個指向該具體型別真實資料的指標

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實現了Transporter介面
var ship Shiper	// Shiper實現了Transporter介面
transport("北京", "天津", car)
transport("北京", "天津", ship)

3. 介面的賦值

// 方法接收者是值
func (car Car) whistle(n int) int {
}
// 方法接收者用指標,則實現介面的是指標型別
func (ship *Shiper) whistle(n int) int {
}
car := Car{}
ship := Shiper{}
var transporter Transporter
transporter = car 
transporter = &car // 值實現的方法,預設指標同樣也實現了
transporter = &ship // 但指標實現的方法,值是沒有實現的

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-->} 

空介面沒有定義任何方法,因此任意型別都實現了空介面

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-->} // 該函數可以接收任意資料型別

注意:slice的元素、map的key和value都可以是空介面型別,map中的key可以是任意能夠用==操作符比較的型別,不能是函數、map、切片,以及包含上述3中型別成員變數的的struct,map的value可以是任意型別

6. 型別斷言

// 若斷言成功,則ok為true,v是具體的型別
if v, ok := i.(int); ok {
	fmt.Printf("i是int型別,其值為%dn", v)
} else {
	fmt.Println("i不是int型別")
}

當要判斷的型別比較多時,就需要寫很多if-else,更好的方法是使用switch i.(type),這也是標準的寫法

switch v := i.(type) { // 隱式地在每個case中宣告了一個變數v
case int:  // v已被轉為int型別
	fmt.Printf("ele is int, value is %dn", v)
	// 在 Type Switch 語句的 case 子句中不能使用fallthrough
case float64: // v已被轉為float64型別
	fmt.Printf("ele is float64, value is %fn", v)
case int8, int32, byte: // 如果case後面跟多種type,則v還是interface{}型別
	fmt.Printf("ele is %T, value is %dn", 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)
	// 順序執行多路召回
	for _, recaller := range rec.Recallers {
		products := recaller.Recall(10) // 統一設定每路最多召回10個商品
		for _, product := range products {
			RecallMap[product.Id] = product // 把多路召回的結果放到map裡,按Id進行排重
		}
	}
	// 把map轉成slice
	RecallSlice := make([]*common.Product, 0, len(RecallMap))
	for _, product := range RecallMap {
		RecallSlice = append(RecallSlice, product)
	}
	SortedResult := rec.Sorter.Sort(RecallSlice) // 對召回的結果進行排序
	// 順序執行多種過濾規則
	FilteredResult := SortedResult
	for _, filter := range rec.Filters {
		FilteredResult = filter.Filter(FilteredResult)
	}
	return FilteredResult
}

面向介面程式設計,在框架層面全是介面。具體的實現由不同的開發者去完成,每種實現單獨放到一個go檔案裡,大家的程式碼互不干擾。通過設定選擇採用哪種實現,也方便進行效果對比

到此這篇關於GoLang函數與面向介面程式設計全面分析講解的文章就介紹到這了,更多相關GoLang函數與面向介面內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com