1. 程式人生 > 實用技巧 >Go語言基礎之函式

Go語言基礎之函式

在Go語言中,函式是一等公民,老牛逼了!

函式

函式定義

  • 關鍵字:func

    func 函式名(引數) (返回值) {
        函式體
    }
    
  • 函式名:由字母、數字、下劃線組成。但函式名的第一個字母不能是數字。在同一個包內,函式名也稱不能重名。

  • 引數:引數由引數變數和引數變數的型別組成,多個引數之間使用,分隔。

  • 返回值:返回值由返回值變數和其變數型別組成,也可以只寫返回值的型別,多個返回值必須用()包裹,並用,分隔。

  • 函式體:實現指定功能的程式碼塊。

函式的呼叫

  • 像Python那樣去呼叫函式

    func sayHello() {
        fmt.Println("hello, world")
    }
    
    func main() {
        sayHello()  // 呼叫函式
    }
    

引數

  • 型別簡寫

    如果傳入引數的型別相同,則可以省略型別,例如:

    func intSum(x, y int) int {
        return x + y
    }
    
  • 可變引數

    可變引數是指函式的引數數量不固定。Go語言中的可變引數通過在引數名後加...來標識。

    注意:可變引數通常要作為函式的最後一個引數。

    func intSum2(x int, y ...int) int {
        // 傳入多個引數,實際上是一個切片
        fmt.Println(x)  
        sum := 0
        for _, v := range y {
            sum += v
        }
        return sum
    }
    
    ret1 := intSum3(100)
    ret2 := intSum3(100, 10)
    ret3 := intSum3(100, 10, 20)
    ret4 := intSum3(100, 10, 20, 30)
    fmt.Println(ret1, ret2, ret3, ret4) //100 110 130 160
    

返回值

  • 多返回值

    Go語言中支援多返回值,函式如果有多個返回值,那麼必須用()將返回值括起來

    func calc(x, y int) (int, int) {
        sum := x + Y
        sub := x - y
        return sum, sub
    }
    
  • 返回值命名

    函式定義時可以給返回值命名,並在函式體中直接使用這些變數,最後通過return關鍵字返回。

    func calc(x, y int) (sum int, sub int) {
        sum := x + Y
        sub := x - y
        return 
    }
    
  • 返回值補充

    當我們的一個函式返回值型別為slice時,nil可以看做是一個有效的slice,沒必要顯示返回一個長度為0的切片。

    func someFunc(x string) []int {
        if x == "" {
            return nil  // 沒必要返回[]int{}
        }
    }
    

函式型別與變數

定義函式型別

我們可以使用type關鍵字來定義一個函式型別,具體格式如下

type calculation func (int, int) int
// 定義了一個 calculation 型別,這是一種函式型別,它接收兩個int型別,返回int型別

凡是滿足這個條件的函式都是calculation型別的函式,例如下面的addsub都是calculation型別

func add(x, y int) int {
    return x + y
}

func sub(x, y int) int {
    return x - y
}

var c calculation
c = add

函式型別變數

我們可以宣告函式型別的變數並且為該變數賦值

func main() {
	var c calculation               // 宣告一個calculation型別的變數c
	c = add                         // 把add賦值給c
	fmt.Printf("type of c:%T\n", c) // type of c:main.calculation
	fmt.Println(c(1, 2))            // 像呼叫add一樣呼叫c

	f := add                        // 將函式add賦值給變數f1
	fmt.Printf("type of f:%T\n", f) // type of f:func(int, int) int
	fmt.Println(f(10, 20))          // 像呼叫add一樣呼叫f
}

高階函式

  • 函式作為引數

    func add(x, y int) int {
    	return x + y
    }
    func calc(x, y int, op func(int, int) int) int {
    	return op(x, y)
    }
    func main() {
    	ret2 := calc(10, 20, add)
    	fmt.Println(ret2) //30
    }
    
  • 函式作為返回值

    func do(s string) (func(int, int) int, error) {
    	switch s {
    	case "+":
    		return add, nil
    	case "-":
    		return sub, nil
    	default:
    		err := errors.New("無法識別的操作符")
    		return nil, err
    	}
    }
    
    do("+")  // 返回add函式的地址和一個nil
    

匿名函式和閉包

匿名函式定義

func (引數) (返回值) {
    函式體
}

匿名函式因為沒有函式名,所以沒辦法像普通函式那樣呼叫,所以匿名函式需要儲存到某個變數或者作為立即執行函式:

func main() {
    // 將匿名函式儲存到變數
    add := func(x, y int) {
        fmt.Println(x + y)
    }
    add(10, 20)  // 通過變數直接呼叫函式
    
    // 自執行函式
    func(x, y int) {
        fmt.Println(x + y)
    }(10, 20)
}

匿名函式多用於實現回撥函式和閉包。

閉包

閉包指的是一個函式和與其相關的引用環境組合而成的實體。簡單來說,閉包=函式+引用環境。 首先我們來看一個例子:

func adder() func(int) int {
	var x int
	return func(y int) int {
		x += y
		return x
	}
}
func main() {
	var f = adder()
	fmt.Println(f(10)) //10
	fmt.Println(f(20)) //30
	fmt.Println(f(30)) //60

	f1 := adder()
	fmt.Println(f1(40)) //40
	fmt.Println(f1(50)) //90
}

變數f是一個函式並且它引用了其外部作用域中的x變數,此時f就是一個閉包。 在f的生命週期內,變數x也一直有效。

閉包進階例項1:

func adder2(x int) func(int) int {
	return func(y int) int {
		x += y
		return x
	}
}
func main() {
	var f = adder2(10)
	fmt.Println(f(10)) //20
	fmt.Println(f(20)) //40
	fmt.Println(f(30)) //70

	f1 := adder2(20)
	fmt.Println(f1(40)) //60
	fmt.Println(f1(50)) //110
}

閉包進階例項2:

func makeSuffixFunc(suffix string) func(string) string {
	return func(name string) string {
		if !strings.HasSuffix(name, suffix) {
			return name + suffix
		}
		return name
	}
}

func main() {
	jpgFunc := makeSuffixFunc(".jpg")
	txtFunc := makeSuffixFunc(".txt")
	fmt.Println(jpgFunc("test")) //test.jpg
	fmt.Println(txtFunc("test")) //test.txt
}

閉包進階例項3:

func calc(base int) (func(int) int, func(int) int) {
	add := func(i int) int {
		base += i
		return base
	}

	sub := func(i int) int {
		base -= i
		return base
	}
	return add, sub
}

func main() {
	f1, f2 := calc(10)
	fmt.Println(f1(1), f2(2)) //11 9
	fmt.Println(f1(3), f2(4)) //12 8
	fmt.Println(f1(5), f2(6)) //13 7
}

閉包=函式+引用環境

defer語句

Go語言中的defer語句會將其後面跟隨的語句進行延遲處理。在defer歸屬的函式即將返回時,將延遲處理的語句按defer定義的逆序進行執行,也就是說,先被defer的語句最後被執行,最後被defer的語句,最先被執行。

func main() {
	fmt.Println("start")
	defer fmt.Println(1)
	defer fmt.Println(2)
	defer fmt.Println(3)
	fmt.Println("end")
}  // 在函式執行完畢之前的最後執行,而且是倒序執行

// 輸出結果
start
end
3
2
1

由於defer語句延遲呼叫的特性,所以defer語句能非常方便的處理資源釋放問題。比如:資源清理、檔案關閉、解鎖及記錄時間等。

defer執行時機

在Go語言的函式中return語句在底層並不是原子操作,它分為給返回值賦值和RET指令兩步。而defer語句執行的時機就在返回值賦值操作後,RET指令執行前。具體如下圖所示:

3543.21

defer經典案例

func f1() int {
	x := 5
	defer func() {
		x++
	}()
	return x
}

func f2() (x int) {
	defer func() {
		x++
	}()
	return 5
}

func f3() (y int) {
	x := 5
	defer func() {
		x++
	}()
	return x
}
func f4() (x int) {
	defer func(x int) {
		x++
	}(x)
	return 5
}
func main() {
	fmt.Println(f1())  5
	fmt.Println(f2())  6
	fmt.Println(f3())  5
	fmt.Println(f4())  5
}

defer面試題

func calc(index string, a, b int) int {
	ret := a + b
	fmt.Println(index, a, b, ret)
	return ret
}

func main() {
	x := 1
	y := 2
	defer calc("AA", x, calc("A", x, y))
	x = 10
	defer calc("BB", x, calc("B", x, y))
	y = 20
}

A 1 2 3
B 10 2 12
BB 10 12 22
AA 1 3 4
//  defer巢狀,內部函式不受影響
//  defer註冊要延遲執行的函式時該函式所有的引數都需要確定其值

內建函式介紹

內建函式 介紹
close 主要用來關閉channel
len 用來求長度,比如string、array、slice、map、channel
new 用來分配記憶體,主要用來分配值型別,比如int、struct。返回的是指標
make 用來分配記憶體,主要用來分配引用型別,比如chan、map、slice
append 用來追加元素到陣列、slice中
panic和recover 用來做錯誤處理

panic/recover

func funcA() {
	fmt.Println("func A")
}

func funcB() {
	panic("panic in B")
}

func funcC() {
	fmt.Println("func C")
}
func main() {
	funcA()
	funcB()
	funcC()
}
func A
panic: panic in B

goroutine 1 [running]:
main.funcB(...)
        .../code/func/main.go:12
main.main()
        .../code/func/main.go:20 +0x98

程式執行期間funcB中引發了panic導致程式崩潰,異常退出了。這個時候我們就可以通過recover將程式恢復回來,繼續往後執行。

func funcA() {
	fmt.Println("func A")
}

func funcB() {
	defer func() {
		err := recover()
		//如果程式出出現了panic錯誤,可以通過recover恢復過來
		if err != nil {
			fmt.Println("recover in B")
		}
	}()
	panic("panic in B")
}

func funcC() {
	fmt.Println("func C")
}
func main() {
	funcA()
	funcB()
	funcC()
}