1. 程式人生 > >go——函式

go——函式

1.定義

函式是結構化程式設計的最小單元模式。它將複雜的演算法過程分解為若干個較小任務,隱藏相關細節,使程式結構更加清晰,易於維護。
函式被設計成相對獨立,通過接收輸入引數完成一段演算法指令,輸出或儲存相關結果。

一個函式的宣告通常包括關鍵字func、函式名、分別由圓括號包裹的列表引數和結果列表
以及由花括號包裹的函式體,就像這樣:

func divide(dividend int,divisor int)(int,error){
	//函式體	
}

函式可以沒有引數列表,也可以沒有結果列表,但空引數列表必須保留括號,而結果列表則不用,示例如下:

func divide(){
	//函式體	
}

另外,引數列表中的必須有名稱,而結果列表中結果的名稱則可有可無。
不過,結果列表中的結果要麼都省略名稱,要麼都要有名稱。
帶有結果名稱的divide函式的宣告如下:

func divide(dividend int,divisor int)(result int,err error){
	//函式體	
}

如果函式的結果有名稱,那麼在函式被呼叫時,以它們為名的變數就會被隱式宣告。
如此一來在函式中就可以直接使用它們了,就像使用引數那樣。
給代表結果的變數賦值,就相當於設定函式的返回結果。

在Go中,函式型別是一等型別,這意味著可以把函式當作一個值來傳遞和使用。
函式值既可以作為其它函式地引數,也可以作為其結果。另外,我們還可以利用函式型別的這一特性來生成閉包。

package main
 
import "fmt"
 
func hello() {
    fmt.Println("hello, world")
}
 
func exec(f func()) { //將函式作為引數
    f()
}
 
func main() {
    f := hello
    exec(f)
}
 
/*
結果:
hello, world
*/

  

函式只能判斷其是否為nil,不支援其它比較操作。

package main
 
import "fmt"
 
func a() {}
func b() {}
 
func main() {
    fmt.Println(a == nil) //false
    fmt.Println(a == b)   //不支援比較操作
    //invalid operation: a == b (func can only be compared to nil)
    //無效操作:a == b (函式只能去判斷其是否為nil)
}

  

從函式返回區域性變數指標是安全的,編譯器會通過逃逸分析來決定是否在堆上分配記憶體。

package main
 
import "fmt"
 
func test() *int { //*int  返回值時指標型別
    a := 0x100
    return &a
}
 
func main() {
    var a *int = test() //定義一個指標型別的變數
    fmt.Println(a, *a)  //a指標變數  *a反向取值
}
 
/*
結果:
0xc00000a168 256
*/

  

 

2.引數

Go對引數的處理偏向保守,不支援有預設值的可選引數,不支援命名實參。
呼叫時,必須按簽名順序傳遞指定型別和數量的實參,就算是“_”命名的引數也不能忽略。

package main

import "fmt"

func test(x, y int, s string, _ bool) *int {
	return nil
}

func main() {
	test(1, 2, "abc") // not enough arguments in call to test定義了四個變數卻只傳遞了三個
}

  

在Go中應該避免在相同層次定義同名變數。

package main

import "fmt"

func add(x, y int) int {  //形參和實參衝突
	x := 100 //no new variables on left side of :=
	var y int
	return x + y
}

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

  

在函式中定義的引數,我們稱之為形參,函式被呼叫時所傳遞的引數我們稱之為實參。
形參類似於函式的區域性變數,而實參則是函式的外部物件,可以是常量、變數、表示式或函式。

package main

import "fmt"

func test(x *int) {
	fmt.Println(&x, x)
}

func main() {
	a := 0x100
	p := &a
	fmt.Println(&p, p)
	test(p)
}

/*
結果:
0xc000076018 0xc00004e080
0xc000076028 0xc00004e080
*/

  

雖然形參和實參都指向一個目標,但是傳遞指標時依然被複制的。

不管傳遞的引數是指標、引用型別,還是其它型別引數,預設採用的都是值拷貝傳遞。
值拷貝傳遞就是在呼叫函式時將實際引數複製一份傳遞到函式中,
這樣在函式中如果對引數進行修改,將不會影響到實際引數。

package main

import "fmt"

func main() {
	var a int = 100
	var b int = 200

	fmt.Printf("交換前a的值為%d\n", a)
	fmt.Printf("交換前b的值為%d\n", b)

	swap(a, b)

	fmt.Printf("交換前a的值為%d\n", a)
	fmt.Printf("交換前b的值為%d\n", b)

}

func swap(x, y int) int {
	var temp int

	temp = x
	x = y
	y = temp

	return temp
}

/*
交換前a的值為100
交換前b的值為200
交換前a的值為100
交換前b的值為200
*/

  另外一種是引用傳遞,就是在呼叫函式時將實際引數的地址傳遞到函式中,那麼在函式中,對引數所進行的修改。

package main

import "fmt"

func main() {
	var a int = 100
	var b int = 200

	fmt.Printf("交換前a的值為%d\n", a)
	fmt.Printf("交換前b的值為%d\n", b)

	swap(&a, &b)  //交換的是指標

	fmt.Printf("交換前a的值為%d\n", a)
	fmt.Printf("交換前b的值為%d\n", b)

}

func swap(x *int, y *int) int {
	var temp int

	temp = *x
	*x = *y
	*y = temp

	return temp
}
/*
交換前a的值為100
交換前b的值為200
交換前a的值為200
交換前b的值為100
*/

  

如果函式的引數過多,建議將其重構為一個複合結構型別。

package main

import (
	"fmt"
	"log"
	"time"
)

type serverOption struct { //定義結構體
	address string
	port    int
	path    string
	timeout time.Duration
	log     *log.Logger
}

func newOption() *serverOption { //以函式的形式返回預設引數
	return &serverOption{
		address: "0.0.0.0",
		port:    8080,
		path:    "/var/test",
		timeout: time.Second * 5,
		log:     nil,
	}
}

func server(option *serverOption) { //需要操作的函式
	fmt.Println(option)
}

func main() {
	opt := newOption()
	opt.port = 8085 //修改屬性值
	server(opt)
}

/*
結果:
&{0.0.0.0 8085 /var/test 5000000000 <nil>}
*/

  

在Go中還有一種稱之為變參的用法,就是一個識別符號傳遞多個引數的用法。
變參本質上就是一個切片。只能接收一到多個同類型的引數,且必須放在列表尾部。
將切片作為變參時,需進行展開操作。如果是陣列,先將其轉換為切片。

package main

import "fmt"

func test(s string, a ...int) {
	fmt.Println(s, a)
}

func main() {
	test("abc", 1, 2, 3, 4, 5)

	x := []int{10, 20, 30}  //使用切片作為變參
	test("abc", x...)

	y := [3]int{40, 50, 60} //使用陣列作為變參
	test("abc", y[:]...)

}
/*
結果:
abc [1 2 3 4 5]
abc [10 20 30]
abc [40 50 60]
*/

  

 

3.返回值

  有返回值的函式,必須有明確的return終止語句。

package main

func test(a int) int { //函式如果有返回值就必須定義返回值的型別
	if a > 0 {
		return 1
	} else if a < 0 {
		return 2 //missing return at end of function
	}
	//邏輯必須完整,這裡缺少一個else
}

func main() {
	test(5)
}

  

函式體中每個條件分支的最後一般都要有return語句,該語句以return關鍵字開始,
後跟與函式結果列表相匹配的變數、常量、表示式或值。
無論是什麼,它們都會被求值並得到確切的值。
但是,如果函式宣告的結果是有名稱的,那麼return關鍵字後面就不用追加任何東西了。

package main

import (
	"errors"
	"fmt"
)

func div(x, y int) (int, error) {
	if y == 0 {
		return 0, errors.New("division by zero")
	}
	return x / y, nil
}

func main() {
	fmt.Println(div(10, 3))
}

  

在Go中沒有元組型別可以用來接收返回值,也不能使用陣列或者切片就收,但是可以用“_”忽略掉不想要的返回值。
多返回值可用作其它函式呼叫實參,或當作結果直接返回。

package main

import (
	"errors"
	"fmt"
)

func div(x, y int) (int, error) {
	if y == 0 {
		return 0, errors.New("division by zero")
	}
	return x / y, nil
}

func log(x int, err error) {
	fmt.Println(x, err)
}

func test() (int, error) {
	return div(5, 0)  //多返回值用作return結果
}

func main() {
	log(test())  //多返回值用作實參
}

  

還有一種對返回值常用的操作是在定義函式的時候給返回值命名。
返回值和引數一樣,可以當作函式區域性變數使用,最後由return隱式返回。

package main

import "fmt"

func div(x, y int) (z int, err error) {
	if y == 0 {
		err = errors.New("division by zero")
		return   //這樣就可以直接進行return
	}
	z := x / y, nil
	return
}

func main() {
	fmt.Println(div(10, 3))
}

  

需要注意的是,這些特殊的“區域性變數”會被不同層級的同名變數遮蔽,這時候就需要使用顯式地return返回。

package main

import "fmt"

func add(x, y int) (z int) {
	{
		z := x + y //新定義的同名區域性變數,同名遮蔽
		return     //z is shadowed during return,改成return z就可以
	}
	return
}

func main() {
	fmt.Println(add(10, 1))
}

  

此外,我們在命名地時候要全部命名,除非返回值能明確表明其含義,就可以省略命名。

package main

func test() (int, s string) { //cannot use 1 (type int) as type string in return argument
	//要麼都命名,要麼都不命名
	return 1, "abc"
}

func main() {
	test()
}

  

4.匿名函式

匿名函式是指沒有定義名字元號地函式。
除沒有名字外,匿名函式和普通函式完全相同。
最大的區別是,我們可在函式內部定義匿名函式,形成類似巢狀效果。
匿名函式常見的使用方法有四種:直接使用、儲存到變數、作為引數和返回值。

(1)直接執行

package main

import "fmt"

func main() {
	func(s string) {
		fmt.Println(s)
	}("科比要結婚了") //直接執行
}

  

  (2)複製給變數

package main

import "fmt"

func main() {
	add := func(x, y int) int {  //賦值給變數
		return x + y
	}
	fmt.Println(add(1, 3))  //4
}

  

  (3)作為引數

package main

import "fmt"

func test(f func()) {
	f()
}

func main() {
	test(func() { //匿名函式作為引數
		fmt.Println("都走吧")
	})
}

  

  (4)作為返回值

package main

import "fmt"

func test() func(int, int) int {  //func(int, int) int:func(int, int)這是返回值名稱,後面的int是返回值的型別
	return func(x, y int) int {  //直接返回一個函式
		return x + y
	}
}

func main() {
	add := test()  //得到一個函式
	fmt.Println(add(11, 22))  傳參
}

  相比與語句塊來說,匿名函式的作用域是被隔離的,不會引發外部汙染,更加靈活。

 

 

5.閉包

在python中,你呼叫了一個函式A,但是函式A直接給你返回一個B函式,那麼B函式就是閉包函式,
在B函式中,呼叫函式A的變數稱作自由變數。
其實在Go語言中,閉包也是類似的定義。

package main

import "fmt"

func test(s string) func() {
	return func() {
		fmt.Println(s)
	}
}

func main() {
	reslut := test("時光悠悠")
	reslut()
}  

test返回的匿名函式會引用上下文環境變數s。
當該函式在main中執行時,它依然可以正確的讀取x的值,這種現象就被稱作閉包。
還有一種閉包的說法就是,如果在一個內部函式裡,對在外部作用域(但不是全域性作用域)的變數進行引用,
那麼內部函式就被認為是閉包。
那麼閉包是如何實現的了?匿名函式被返回之後,為何還能讀取環境的變數值了?

package main

import "fmt"

func test(s string) func() {
	fmt.Println(&s, s)

	return func() {
		fmt.Println(&s, s)
	}
}

func main() {
	reslut := test("時光悠悠")
	reslut()
}

/*
0xc0000421c0 時光悠悠
0xc0000421c0 時光悠悠
*/

  

通過指標我們發現閉包直接引用了原環境變數。
正是因為閉包通過指標引用環境變數,那麼可能會導致其生命週期延長。
但這有時候會帶來一些問題。

package main

import "fmt"

func test() []func() {
	var s []func() //定義一個數組

	for i := 0; i < 2; i++ {
		s = append(s, func() { //新增元素
			fmt.Println(&i, i)
		})
	}
	return s                   //返回匿名函式列表
}

func main() {
	for _, f := range test() {  //迭代執行所有匿名函式
		f()
	}
}

/*
0xc00000a168 2
0xc00000a168 2
*/

  

這並不是我們想要看到的結果,為什麼了?
for迴圈複用區域性變數i,那麼每次新增的匿名函式引用的就是同一個變數。
新增元素的操作僅僅是將匿名函式放入列表而並沒有執行,所以就會出現這種狀況。
解決辦法就是每次使用不同的環境變數或傳參複製,讓各自閉包環境各不相同。

package main

import "fmt"

func test() []func() {
	var s []func() //定義一個數組

	for i := 0; i < 2; i++ {
		x := i
		s = append(s, func() { //新增元素
			fmt.Println(&x, x)
		})
	}
	return s
}

func main() {
	for _, f := range test() {
		f()
	}
}

/*
結果:
0xc00000a168 0
0xc00000a180 1
*/

  

多個匿名函式引用同一變數時,任何修改都會影響其它函式的取值,這是我們需要注意的。

package main

import "fmt"

func test(x int) (func(), func()) { //返回兩個匿名函式
	return func() {
			fmt.Println(x)
			x += 10 //修改環境變數
		}, func() {
			fmt.Println(x)
		}
}

func main() {
	a, b := test(100)
	a() //100
	b() //110
}

  所以對於閉包應該慎用。

 

6.延遲呼叫

除了前面介紹的流程控制語句外,Go還有一些特有的流程控制語句,其中一個就是defer。
該語句用於延遲呼叫指定的函式,它只能出現在函式內部,由defer關鍵字以及針對某個函式的呼叫表示式組成。
這裡被呼叫的函式稱為延遲函式。

func outerFunc() {
	defer fmt.Println("函式執行結束前一刻才會被列印")  //延遲執行,呼叫fmt.PrintLn
	fmt.Println("第一個被列印")    //延遲函式
}

其中,defer關鍵字後面是針對fmt.Println函式的呼叫表示式。程式碼裡面也說明了延遲函式的執行時機。
這裡的outerFunc稱為外圍函式,呼叫outerFunc的那個函式稱為呼叫函式。
下面是具體的規則:
  a.當外圍函式中的語句正常執行完畢時,只有其中所有的延遲函式都執行完畢,外圍函式才會真正結束執行。
  b.當執行外圍函式中的return語句時,只有其中所有的延遲函式都執行完畢後,外圍函式才會真正返回。
  c.當外圍函式中的程式碼引發執行異常,只有其中所有的延遲函式都執行完畢後,該執行異常才會被真正擴散至呼叫函式。
正因為defer語句有這樣的特性,所以它成為了執行釋放資源或異常處理等收尾任務的首選。
明顯的優勢有以下兩個:
  A.對延遲函式的呼叫總會在外圍函式執行結束前執行。
  B.defer語句在外圍函式體中的位置不限,並且數量不限。

不過,使用defer語句還有三點需要注意:
第一點,如果在延遲函式中使用外部變數,就應該通過引數傳入。

func printNumbers() {
	for i := 0; i < 5; i++ {
		defer func() {
			fmt.Printf("%d", i)
		}()
	}
}

上述程式碼的執行結果是55555,這正是由於延遲函式的執行實際引起的,for迴圈執行完畢之後,才會執行延遲函式。
也就是說,當執行延遲函式的時候,i已經等於5了。

func printNumbers() {
	for i := 0; i < 5; i++ {
		defer func(n,int) {
			fmt.Printf("%d", n)
		}(i)
	}
}

列印的內容會是43210,至於為什麼,請看下面的第二點。

第二點:同一個外圍函式內多個延遲函式呼叫的執行順序,會與其所屬的defer語句的執行順序完全相反。
同一個外圍函式中的每個defer語句執行的時候,針對其延遲函式的呼叫表示式都會被壓入同一個棧。
在該外圍函式執行結束的那一刻,Go會從這個棧中依次取出,棧的取值順序是先進後出。

第三點:延遲函式呼叫,若有引數傳入,那麼那些引數的值會在當前defer語句執行時求出。

func printNumbers() {
	for i := 0; i < 5; i++ {
		defer func(n,int) {
			fmt.Printf("%d", n)
		}(i*2)
	}
}

此時,執行的結果是86420

最後看一個例子:

package main

import (
	"fmt"
)

func main() {
	x, y := 1, 3

	defer func(a int) {
		fmt.Println("defer x, y=", a, y) //y為閉包引用
	}(x) //註冊時複製呼叫引數,所以x為1

	x += 100
	y += 200
	fmt.Println(x, y)
}

/*
101 203
defer x, y= 1 203
*/

  

 

7.錯誤處理

(1)error

標準庫將error定義為介面型別,以便發現自定義錯誤型別。

type error interface {
	Error() string	 
}

按慣例,error總是最後一個返回引數。標準庫提供了相關建立函式,
可以很方便的建立包含簡單錯誤處理文字的error物件。

package main

import (
	"errors"
	"fmt"
	"log"
)

var errDivByZero = errors.New("division by zero")

func div(x, y int) (int, error) {
	if y == 0 {
		return 0, errDivByZero
	}
	return x / y, nil
}

func main() {
	z, err := div(5, 0)
	if err == errDivByZero {
		log.Fatalln(err)
	}

	fmt.Println(z)
}

/*
2018/11/30 03:15:24 division by zero
exit status 1
*/

  

某些時候,我們需要自定義錯誤型別,以便容納更多的上下文狀態資訊。

package main

import (
	"fmt"
	"log"
)

type DivError struct {
	x, y int
}

func (DivError) Error() string {
	return "division by zero"
}

func div(x, y, int) (int, error) {
	if y == 0 {
		return 0, DivError{x, y}
	}
	return x / y, nil
}

func main() {
	z, err := div(5, 0)
	if err != nil {
		switch e := err.(type) {
		case DivError:
			fmt.Println(e, e.x, e.y)
		default:
			fmt.Println(e)
		}
		log.Fatalln(err)
	}
	fmt.Println(z)
}

  

(2)panic和recover

panic會立即中斷當前函式流程,執行延遲呼叫。
而在延遲函式中,recover可捕獲並返回panic提交的錯誤物件。

package main

import (
	"fmt"
	"log"
)

func main() {
	defer func() {
		if err := recover(); err != nil {
			log.Fatalln(err)
		}
	}()

	panic("I dead")
	fmt.Println("exit!")
}

/*
2018/11/30 03:35:18 I dead
exit status 1
*/

  

因為panic引數是空介面型別,因此可以使用任何物件作為錯誤狀態。
無論是否執行recover,所有延遲都會被執行。
但中斷性錯誤會呼叫堆疊向外傳遞,要麼被外層捕獲,要麼導致程序崩潰。t/css" /> 從函式返回區域性變數指標是安全的,編譯器會通過逃逸分析來決定是否在堆上分配記憶體。

package main

import (
	"fmt"
	"log"
)

func test() {
	defer fmt.Println("test.1")
	defer fmt.Println("test.2")

	panic("I dead")
}

func main() {
	defer func() {
		log.Fatalln(recover())
	}()

	test()
}

/*
test.2
test.1
2018/11/30 03:39:02 I dead
exit status 1
*/

  連續呼叫panic,僅最後一個會被recover捕獲。