1. 程式人生 > 其它 >Golang通脈之函式

Golang通脈之函式

函式是組織好的、可重複使用的、用於執行指定任務的程式碼塊。

Go語言中支援函式、匿名函式和閉包,並且函式在Go語言中屬於“一等公民”。

函式定義

Go語言中定義函式使用func關鍵字,具體格式如下:

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

其中:

  • 函式宣告:關鍵字func
  • 函式名:由字母、數字、下劃線組成。但函式名的第一個字母不能是數字。在同一個包內,函式名稱不能重名。
  • 函式引數:引數由引數變數和引數變數的型別組成,引數變數可以省略,可以有一個引數,也可以有多個,也可以沒有;多個引數之間使用,分隔;多個引數時引數變數要麼全寫,要麼全省略;如果多個相鄰引數的型別是一樣的,可以只保留同一型別最後一個引數的宣告。
  • 函式返回值:返回值由返回值變數和其變數型別組成,返回值變數可以省略,可以有一個返回值,也可以有多個,也可以沒有;多個返回值必須用()包裹,並用,分隔;多個返回值時返回值變數要麼全寫,要麼全省略。
  • 函式體:實現指定功能的邏輯。

定義一個求兩個數之和的函式:

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

函式的引數和返回值都是可選的,實現一個既不需要引數也沒有返回值的函式:

func printf() {
	fmt.Println("printf函式")
}

函式的呼叫

定義了函式之後,可以通過函式名()的方式呼叫函式。 呼叫上面定義的兩個函式:

func main() {
	printf()
	ret := add(10, 20)
	fmt.Println(ret)
}

注意,呼叫有返回值的函式時,可以不接收其返回值。

引數

引數使用

形式引數:定義函式時,用於接收外部傳入的資料,叫做形式引數,簡稱形參。

實際引數:呼叫函式時,傳給形參的實際的資料,叫做實際引數,簡稱實參。

函式呼叫:

A:函式名稱必須匹配
B:實參與形參必須一一對應:順序,個數,型別

型別簡寫

函式的引數中如果相鄰變數的型別相同,則可以省略型別,例如:

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

如果多個相鄰引數的型別是一樣的,可以只保留同一型別最後一個引數的宣告,add

函式有兩個引數,這兩個引數的型別均為int,因此可以省略x的型別,因為y後面有型別說明,x引數也是該型別。

可變引數

函式的引數數量是可變的,比如最常見的 fmt.Println 函式。Go語言中的可變引數通過在引數名後加...來標識。可變引數在函式體中是切片型別

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

舉個例子:

func add(x ...int) int {
	fmt.Println(x) //x是一個切片
	sum := 0
	for _, v := range x {
		sum = sum + v
	}
	return sum
}

呼叫上面的函式:

ret1 := add()
ret2 := add(10)
ret3 := add(10, 20)
ret4 := add(10, 20, 30)
fmt.Println(ret1, ret2, ret3, ret4) //0 10 30 60

固定引數搭配可變引數使用時,可變引數要放在固定引數的後面,示例程式碼如下:

func add(x int, y ...int) int {
	fmt.Println(x, y)
	sum := x
	for _, v := range y {
		sum = sum + v
	}
	return sum
}

呼叫上述函式:

ret5 := add(100)
ret6 := add(100, 10)
ret7 := add(100, 10, 20)
ret8 := add(100, 10, 20, 30)
fmt.Println(ret5, ret6, ret7, ret8) //100 110 130 160

本質上,函式的可變引數是通過切片來實現的。

引數傳遞

go語言函式的引數也是存在值傳遞引用傳遞

值傳遞

func main(){
   /* 宣告函式變數 */
   getSquareRoot := func(x float64) float64 {
      return math.Sqrt(x)
   }

   /* 使用函式 */
   fmt.Println(getSquareRoot(9))

}

引用傳遞(涉及指標知識點)

這就牽扯到了所謂的指標。我們知道,變數在記憶體中是存放於一定地址上的,修改變數實際是修改變數地址處的內 存。只有add1函式知道x變數所在的地址,才能修改x變數的值。所以需要將x所在地址&x傳入函式,並將函式的引數的型別由int改為*int,即改為指標型別,才能在函式中修改x變數的值。此時引數仍然是按copy傳遞的,只是copy的是一個指標:

//簡單的一個函式,實現了引數+1的操作
func add1(a *int) int { // 請注意,
    *a = *a+1 // 修改了a的值
    return *a // 返回新值
} 
func main() {
    x := 3
    fmt.Println("x = ", x) // 應該輸出 "x = 3"
    x1 := add1(&x) // 呼叫 add1(&x) 傳x的地址
    fmt.Println("x+1 = ", x1) // 應該輸出 "x+1 = 4"
    fmt.Println("x = ", x) // 應該輸出 "x = 4"
}
  • 傳指標使得多個函式能操作同一個物件。
  • 傳指標比較輕量級 (8bytes),只是傳記憶體地址,可以用指標傳遞體積大的結構體。如果用引數值傳遞的話, 在每次copy上面就會花費相對較多的系統開銷(記憶體和時間)。所以要傳遞大的結構體的時候,用指標是一個明智的選擇。
  • Go語言中slice,map這三種類型的實現機制類似指標,所以可以直接傳遞,而不用取地址後傳遞指標。(注:若函式需改變slice的長度,則仍需要取地址傳遞指標)

返回值

Go語言中通過return關鍵字向外輸出返回值。

多返回值

Go語言中函式支援多返回值,函式如果有多個返回值時必須用()將所有返回值包裹起來:

func swap(x, y string) (string, string) {
   return y, x
}

返回值命名

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

func SumAndProduct(a, b int) (add int, multiplied int) {
    add = a + b
    multiplied = a * b
    return
}

返回值補充

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

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

空白識別符號

_ 是Go中的空白識別符號。它可以代替任何型別的任何值。

比如rectProps函式返回的結果是面積和周長,如果只要面積,不要周長,就可以使用空白識別符號:

func rectProps(length, width float64) (float64, float64) {  
    var area = length * width
    var perimeter = (length + width) * 2
    return area, perimeter
}
func main() {  
    area, _ := rectProps(10.8, 5.6) // perimeter is discarded
    fmt.Printf("Area %f ", area)
}

函式進階

變數作用域

作用域:變數可以使用的範圍。

全域性變數

全域性變數是定義在函式外部的變數,它在程式整個執行週期內都有效。 所有的函式都可以使用,而且共享這一份資料

//定義全域性變數num
var num int64 = 10

func testGlobalVar() {
	fmt.Printf("num=%d\n", num) //函式中可以訪問全域性變數num
}
func main() {
	testGlobalVar() //num=10
}

區域性變數

區域性變數又分為兩種: 變數在哪裡定義,就只能在哪個範圍使用,超出這個範圍,變數就被銷燬了:

func testLocalVar() {
	//定義一個函式區域性變數x,僅在該函式內生效
	var x int64 = 100
	fmt.Printf("x=%d\n", x)
}

func main() {
	testLocalVar()
	fmt.Println(x) // 此時無法使用變數x
}

如果區域性變數和全域性變數重名,優先訪問區域性變數

package main

import "fmt"

//定義全域性變數num
var num int64 = 10

func testNum() {
	num := 100
	fmt.Printf("num=%d\n", num) // 函式中優先使用區域性變數
}
func main() {
	testNum() // num=100
}

語句塊定義的變數:通常會在if條件判斷、for迴圈、switch語句上使用這種定義變數的方式。

func testLocalVar2(x, y int) {
	fmt.Println(x, y) //函式的引數也是隻在本函式中生效
	if x > 0 {
		z := 100 //變數z只在if語句塊生效
		fmt.Println(z)
	}
	//fmt.Println(z)//此處無法使用變數z
}

for迴圈語句中定義的變數,也是隻在for語句塊中生效:

func testLocalVar3() {
	for i := 0; i < 10; i++ {
		fmt.Println(i) //變數i只在當前for語句塊中生效
	}
	//fmt.Println(i) //此處無法使用變數i
}

函式型別與變數

定義函式型別

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

type calculation func(int, int) int

上面語句定義了一個calculation型別,它是一種函式型別,這種函式接收兩個int型別的引數並且返回一個int型別的返回值。

簡單來說,凡是滿足這個條件的函式都是calculation型別的函式,例如下面的add和sub是calculation型別。

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

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

add和sub都能賦值給calculation型別的變數。

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

匿名函式和閉包

匿名函式

函式當然還可以作為返回值,但是在Go語言中函式內部不能再像之前那樣定義函數了,只能定義匿名函式。匿名函式就是沒有函式名的函式,匿名函式的定義格式如下:

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指令執行前。具體如下圖所示:

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
}

問,上面程式碼的輸出結果是?(提示:defer註冊要延遲執行的函式時該函式所有的引數都需要確定其值)

defer注意點

當外圍函式中的語句正常執行完畢時,只有其中所有的延遲函式都執行完畢,外圍函式才會真正的結束執行。
當執行外圍函式中的return語句時,只有其中所有的延遲函式都執行完畢後,外圍函式才會真正返回。
當外圍函式中的程式碼引發執行恐慌時,只有其中所有的延遲函式都執行完畢後,該執行時恐慌才會真正被擴充套件至呼叫函式。

內建函式介紹

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

panic/recover

Go語言中目前(Go1.12)是沒有異常機制,但是使用panic/recover模式來處理錯誤。 panic可以在任何地方引發,recover只有在defer呼叫的函式中有效。 首先來看一個例子:

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

注意:

  1. recover()必須搭配defer使用。
  2. defer一定要在可能引發panic的語句之前定義。