Go語言入門系列(六)之再探函式

行人觀學發表於2020-08-11

Go語言入門系列前面的文章:

Go語言入門系列(二)之基礎語法總結這篇文章中已經介紹過了Go語言的函式的基本使用,包括宣告、引數、返回值。本文再詳細介紹一下函式的其他使用。

1. 變參

Go語言的函式除了支援0個或多個引數,還支援不定數量的引數,即變參。宣告方式為:

func foo(變參名 ...引數型別) 函式型別 {
    //函式體
}

下面是一個具體的函式,它接收不定數量的int引數,並返回和:

package main

import "fmt"

func add(arg ...int) int { //變參函式
	var sum int
	for _, value := range arg {
		sum += value
	}
	return sum
}

func main() {
	sum := add(1, 2, 3, 4)
	fmt.Println(sum) //10
}

arg ...int表明add函式接收不定數量的引數,且只能是int型別的。arg是我們給該變參取的名字,它實際上是一個切片,所以在add函式中可以使用range遍歷變數arg

2. 傳值和傳指標

當我們呼叫一個有參函式時,肯定會向該函式中傳入引數:

package main

import "fmt"

//傳入一個值,列印它
func printX(x int)  {
	fmt.Println(x)
}

func main() {
	var a int = 5
	printX(a) //向函式中傳入引數:變數a
}

這裡有一個問題:我們真的是把變數a傳給了printX函式嗎?我們用兩個例子來說明問題。

2.1. 例1

package main

import "fmt"

func plusOne(x int) int {
	x = x + 1
	fmt.Println("執行加一")
	return x
}

func main() {
	a := 5
	fmt.Println("a =", a) //應該為5 實際為5
	b := plusOne(a)
	fmt.Println("a =", a) //應該為6 實際為5
	fmt.Println("b =", b) //應該為6 實際為6
}

plusOne函式的作用是把傳進來的引數加一,並返回結果。

a=5傳進plusOne函式,執行了x = x + 1語句,那麼執行過後a的值應該為6,但實際為5。

變數b接收了函式的返回值,所以為6,這沒問題。

這證明了,我們的a變數根本就沒傳進函式中,那麼實際傳的是什麼?實際傳的是a變數的一份拷貝。

所以,我們向Go語言中的函式傳入一個值,實際上傳的是該值的拷貝,而非該值本身。

那如果我們確實要把上例中的變數a傳入plusOne函式中呢?那此時就不應該傳值了,而是應該傳入指標。程式碼改進如下:

package main

import "fmt"

func plusOne(x *int) int { //引數是指標變數
	*x = *x + 1
	fmt.Println("執行加一")
	return *x
}

func main() {
	a := 5
	fmt.Println("a =", a) //應該為5 實際為5
	b := plusOne(&a) //傳入地址
	fmt.Println("a =", a) //應該為6 實際為6
	fmt.Println("b =", b) //應該為6 實際為6
}

a=5傳進plusOne函式,執行了x = x + 1語句,執行過後a的值實際為6。

這就證明,變數a確實被傳進plusOne函式並被修改了。因為我們傳進去的是一個指標,即變數的地址,有了地址我們可以直接操作變數。

如果你對指標的使用不熟悉,這裡的程式碼可能會有點難理解,下面逐行解釋:

func plusOne(x *int) int { 

宣告x是一個int型別的指標引數,只接受int型別變數的地址 。

*x = *x + 1

使用*操作符根據x中存的地址,獲取到對應的值,然後加一。

return *x

使用*操作符根據x中存的地址,獲取到對應的值,然後返回。

b := plusOne(&a) 

plusOne函式只接受int型別變數的地址,所以使用&操作符獲取a變數的地址,然後才傳入。

2.2. 例2

下面我再舉一個經典的例子:寫一個函式,能夠交換兩個變數的值。

如果你不知道什麼是傳值和傳指標,那可能會寫成這樣:

package main

import "fmt"

func swap(x, y int) {
	tmp := x
	x = y
	y = tmp
	fmt.Println("函式中:x =", x, ", y =", y)
}

func main()  {
	x, y := 2, 8
	fmt.Println("交換前:x =", x, ", y =", y)
	swap(x, y)
	fmt.Println("交換後:x =", x, ", y =", y)
}

執行結果:

交換前:x = 2 , y = 8
函式中:x = 8 , y = 2
交換後:x = 2 , y = 8

只在函式中完成了交換,出了函式又變回原樣了。

想要完成交換,就必須傳入指標,而非值拷貝:

package main

import "fmt"

func swap(x, y *int) {
	tmp := *x
	*x = *y
	*y = tmp
	fmt.Println("函式中:x =", *x, ", y =", *y)
}

func main()  {
	x, y := 2, 8
	fmt.Println("交換前:x =", x, ", y =", y)
	swap(&x, &y)
	fmt.Println("交換後:x =", x, ", y =", y)
}

執行結果:

交換前:x = 2 , y = 8
函式中:x = 8 , y = 2
交換後:x = 8 , y = 2

傳入指標能夠真正交換兩個變數的值。

傳入指標的好處:

  1. 傳入指標使我們能夠在函式中直接操作變數,多個函式也能操作同一個變數。
  2. 不需要再拷貝一遍值了。如果你需要傳入比較大的結構體,再拷貝一遍就多花費系統開銷了,而傳入指標則小的多。

3. 函式作為值

在Go語言中,函式也可以作為值來傳遞。下面是一個例子:

package main

import "fmt"

type calculate func(int, int) int // 宣告瞭一個函式型別

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

func product(x, y int) int {
	return x * y
}

func choose(a, b int, f calculate) int { //函式作為引數
	return f(a, b)
}

func main(){
	diff := func(x, y int) int { //函式作為值賦給diff
		return x - y
	}

	fmt.Println(choose(2, 3, sum)) //5
	fmt.Println(choose(4, 5, product)) //20
	fmt.Println(choose(6, 7, diff)) //-1
	fmt.Println(diff(9, 8)) //1
}

函式作為值或者引數肯定要有對應的型別,型別是:func(引數型別)返回值型別 。比如func(int,int) int

可以使用type關鍵字給func(int,int) int起個別名叫calculate,方便使用。

choose函式中宣告瞭一個型別為calculate的函式引數f,而我們又編寫了calculate型別的函式sumproduct,所以可以向choose函式中傳入這兩個函式。

我們給變數diff賦了一個函式,所以能夠使用diff(9, 8),或者將其作為引數傳入choose函式。

作者簡介

我是「行小觀」,於千萬人中的一個普通人。陰差陽錯地走上了程式設計這條路,既然走上了這條路,那麼我會盡可能遠地走下去。

我會在公眾號『行人觀學』中持續更新「Java」、「Go」、「資料結構和演算法」、「計算機基礎」等相關文章。

歡迎關注,我們一起踏上行程。

本文章屬於系列文章「Go語言入門系列」。

如有錯誤,還請指正。

相關文章