1. 程式人生 > >Go程式設計基礎—函式(func)

Go程式設計基礎—函式(func)

https://blog.csdn.net/qq_22063697/article/details/74858264

函式是基本的程式碼塊,用於執行一個任務,是構成程式碼執行的邏輯結構。
在Go語言中,函式的基本組成為:關鍵字func、函式名、引數列表、返回值、函式體和返回語句。

函式定義

函式其實在之前已經見過了,第一次執行hello world程式的main()其實就是一個函式,而且是一個比較特殊的函式。每個go程式都是從名為main的package包的main()函式開始執行,包的概念不是這裡的重點,以後做單獨說明。同時main()函式是無引數,無返回值的。
Go函式的完成定義如下:

func function_name( [parameter list] ) [return_types] {
函式體
}
1
2
3
定義解析:
從Go的函式定義可以看出,Go的返回值是放在函式名和引數後面的,這點和C及Java的差別還是很多大的。

func:Go的函式宣告關鍵字,宣告一個函式。

function_name:函式名稱,函式名和引數列表一起構成了函式簽名。

parameter list:引數列表,指定的是引數型別、順序、及引數個數。引數是可選的,即函式可以不包含引數。引數就像一個佔位符,這是引數被稱為形參,當函式被呼叫時,將具體的值傳遞給引數,這個值被稱為實際引數。

return_types:返回型別,函式返回一列值。return_types 是該列值的資料型別。這裡需要注意的是Go函式支援多返回值。有些功能不需要返回值,這種情況下 return_types 不是必須的。

函式體:函式定義的程式碼集合,表示函式完成的動作。
1
2
3
4
5
6
7
8
9
函式呼叫

Go的函式呼叫只要通過函式名然後向函式傳遞引數,函式就會執行並返回值回來。就像之前呼叫Println()輸出資訊一樣。
這裡提一點,如果函式和呼叫不在同一個包(package)內,需要先通過import關鍵字將包引入–import “fmt”。函式Println()就屬於包fmt。
這裡可能注意到Println()函式命名是首字母是大寫的。在Go語言中函式名字的大小寫不僅僅是風格,更直接體現了該函式的可見性。這和其他語言對於函式或方法的命名規定可能有很大不同,像Java就推薦是駝峰的寫法,C也不建議函式名首字母是大寫。但是在Go中,如果首字母不大寫,你可能會遇到莫名其妙的編譯錯誤, 比如你明明匯入了對應的包,Go編譯器還是會告訴你無法找到這個函式。
因此在Go中,需要記住一個規則:

小寫字母開頭的函式只在本包內可見,大寫字母開頭的函式才能被其他包使用。
同時這個規則也適用於變數的可見性,即首字母大寫的變數才是全域性的。

package main

import “fmt”

/* 函式返回兩個數的較大值 /func max(num1 int, num2 int) int {
/
定義區域性變數 */
var result int

if num1 > num2 {
    result = num1
} else {
    result = num2
}
return result

}

func main() {
var a int = 100
var b int = 200
var ret int
/* 呼叫函式並返回較大值 */
ret = max(a, b)

fmt.Printf("最大值是 : %d\n", ret)

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

上面定義了一個函式max(),用於比較兩個數,並返回其中較大的一個。最終通過main() 函式中呼叫 max()函式執行。
這裡關於函式的引數列表有一個簡便寫法,當連續兩個或多個函式的已命名形參型別相同時,除最後一個型別以外,其它都可以省略。
就像上面的func max(num1 int, num2 int) int {}定義,可以簡寫成

func max(num1 , num2 int) int {}
1
多返回值

前面定義函式時說過,Go的函式支援多返回值,這與C、C++和Java等開發語言極大不同。這個特效能夠使我們寫出比其他語言更優雅、更簡潔的程式碼,比如File.Read()函 數就可以同時返回讀取的位元組數和錯誤資訊。如果讀取檔案成功,則返回值中的n為讀取的位元組 數,err為nil,否則err為具體的出錯資訊:

func (file *File) Read(b []byte) (n int, err Error)
1
一個簡單的例子如下:

package main

import “fmt”

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

func main() {
a, b := swap(“hello”, “world”)
fmt.Println(a, b)
}
1
2
3
4
5
6
7
8
9
10
11
12

上面實現了簡單的字串交換功能,程式碼實現上十分的簡潔,因為支援多返回值,所以不需要想Java需要構建一個可以儲存多個值得資料結構。
而且可以發現,對於返回值如果是同一型別,可以不定義變數名稱,雖然程式碼看上去是簡潔了很多,但是命名後的返回值可以讓程式碼更清晰,可讀性更強。

如果呼叫方呼叫了一個具有多返回值的方法,但是卻不想關心其中的某個返回值,可以簡單 地用一個下劃線“_”來跳過這個返回值。就像上面的例子,如果我們只關注第一個返回值則可以寫成:

a, _ := swap(“hello”, “world”)
1
若值關注第二返回值則可以寫成:

_, b := swap(“hello”, “world”)
1
函式引數

函式定義時指出,函式定義時有引數,該變數可稱為函式的形參。形參就像定義在函式體內的區域性變數。
但當呼叫函式,傳遞過來的變數就是函式的實參,函式可以通過兩種方式來傳遞引數:

值傳遞:指在呼叫函式時將實際引數複製一份傳遞到函式中,這樣在函式中如果對引數進行修改,將不會影響到實際引數。
引用傳遞:是指在呼叫函式時將實際引數的地址傳遞到函式中,那麼在函式中對引數所進行的修改,將影響到實際引數。
1
2
在預設情況下,Go 語言使用的是值傳遞,即在呼叫過程中不會影響到實際引數。

值傳遞

Go中int型別儲存的的是一個數字型別,下面定義一個交換函式swap(),用於交換兩個引數的值。

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 的值 */
x = y    /* 將 y 值賦給 x */
y = temp /* 將 temp 值賦給 y*/

return temp

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

引用引數

通過前面的介紹可以知道,Go的指標型別是對變數地址的引用。
將上面的swap()做些修改,引數接受兩個指標型別,然後做交換。

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 指向 a 指標,a 變數的地址
 * &b 指向 b 指標,b 變數的地址
 */
swap(&a, &b)

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

}

/* 定義相互交換值的函式 */func swap(x, y *int) {
var temp int

temp = *x /* 儲存 x 的值 */
*x = *y   /* 將 y 值賦給 x */
*y = temp /* 將 temp 值賦給 y*/

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

可以發現,最終傳進來的引數指在執行交換函式swap()後也被修改了,這是因為引數最終指向的都是地址的引用,所有引用被修改了,值也就相應的變了。

不定引數

顧名思義,不定引數就是函式的引數不是固定的。這個在C和Java裡都有。在之前的程式碼中,包fmt下面的 fmt.Println()函式也是引數不定的。

不定引數型別

先看一個函式的定義:

func myfunc(args …int) {
}
1
2
可以看出,上面的定義和之前的函式定義最大的不同就是,他的引數是以“…type”的方式定義的,這和Java的語法有些類似,也是用”…”實現。需要說明的是“…type”在Go中只能作為引數的形式出現,而且只能作為函式的最後一個引數。
從內部實現機理上來說,型別“…type“本質上是一個數組切片,也就是[]type,這點可以從下面的一個小程式驗證一下。

package main

import “fmt”

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

myfunc(a, b)

}

func myfunc(args …int) {
fmt.Println(args)
for _, arg := range args {
fmt.Println(arg)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

從上面的結果可以看出,型別“…type“本質上是一個數組切片,也就是[]type,所以引數args可以用for迴圈來獲得每個傳入的引數。
這是Go的一 個語法糖(syntactic sugar),即這種語法對語言的功能並沒有影響,但是更方便程式設計師使用。通常來說,使用語法糖能夠增加程式的可讀性,從而減少程式出錯的機會。

不定引數的傳遞

同樣是上面的myfunc(args …int)函式為例,在引數賦值時可以不用一個一個的賦值,可以直接傳遞一個數組或者切片,特別注意的是在引數後加上“…”即可。

package main

import “fmt”

func main() {
arr := []int{100, 200, 300}

myfunc(arr...)
myfunc(arr[:2]...)

}

func myfunc(args …int) {
fmt.Println(args)
for _, arg := range args {
fmt.Println(arg)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

任意型別的不定引數

上面的例子在定義不定引數時,都有一個要求,引數的型別是一致的。那麼如果函式的引數型別不一致,如何使用不定引數方式來定義。在Go中,要實現這個需求需要引入一個新的型別–interface{}。看名字可以看出,這種型別實際上就是介面。關於Go的介面這裡只做引出。
看一下之前常用的Printf函式的定義,位置在Go的src目錄下的print.go檔案中。

func Println(a …interface{}) (n int, err error) {
return Fprintln(os.Stdout, a…)
}
1
2
3
其實用interface{}傳遞任意型別資料是Go語言的慣例用法,而且interface{}是型別安全的。
看下面的例子:

package main

import (
“fmt”
“reflect”
)

func main() {
arr := []int{100, 200, 300}
myfunc(100, “abc”, arr)

}

func myfunc(args …interface{}) {
fmt.Println(args)
for _, arg := range args {
fmt.Println(arg)
fmt.Println(reflect.TypeOf(arg))
fmt.Println("=======")
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

匿名函式

匿名函式是指不需要定義函式名的一種函式實現方式。1958年LISP首先採用匿名函式。
在Go裡面,函式可以像普通變數一樣被傳遞或使用,Go語言支援隨時在程式碼裡定義匿名函式。
匿名函式由一個不帶函式名的函式宣告和函式體組成。匿名函式的優越性在於可以直接使用函式內的變數,不必申明。
直接看一個例子:

package main

import (
“fmt”
“math”
)

func main() {
getSqrt := func(a float64) float64 {
return math.Sqrt(a)
}
fmt.Println(getSqrt(4))

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

上面先定義了一個名為getSqrt 的變數,初始化該變數時和之前的變數初始化有些不同,使用了func,func是定義函式的,可是這個函式和上面說的函式最大不同就是沒有函式名,也就是匿名函式。這裡將一個函式當做一個變數一樣的操作。

閉包
理解閉包

閉包的應該都聽過,但到底什麼是閉包呢?
閉包是由函式及其相關引用環境組合而成的實體(即:閉包=函式+引用環境)。
“官方”的解釋是:所謂“閉包”,指的是一個擁有許多變數和綁定了這些變數的環境的表示式(通常是一個函式),因而這些變數也是該表示式的一部分。
維基百科講,閉包(Closure),是引用了自由變數的函式。這個被引用的自由變數將和這個函式一同存在,即使已經離開了創造它的環境也不例外。所以,有另一種說法認為閉包是由函式和與其相關的引用環境組合而成的實體。閉包在執行時可以有多個例項,不同的引用環境和相同的函式組合可以產生不同的例項。
看著上面的描述,會發現閉包和匿名函式似乎有些像。可是可能還是有些雲裡霧裡的。因為跳過閉包的建立過程直接理解閉包的定義是非常困難的。目前在JavaScript、Go、PHP、Scala、Scheme、Common Lisp、Smalltalk、Groovy、Ruby、 Python、Lua、objective c、Swift 以及Java8以上等語言中都能找到對閉包不同程度的支援。通過支援閉包的語法可以發現一個特點,他們都有垃圾回收(GC)機制。
javascript應該是普及度比較高的程式語言了,通過這個來舉例應該好理解寫。看下面的程式碼,只要關注script裡方法的定義和呼叫就可以了。

<title></title></head><body>  

$(function(){
var c=a();
c();
c();
c();
//a(); //不會有資訊輸出
document.write("

=============

");
var c2=a();
c2();
c2();
});

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24

這段程式碼有兩個特點:

函式b巢狀在函式a內部
函式a返回函式b
1
2
這樣在執行完var c=a()後,變數c實際上是指向了函式b(),再執行函式c()後就會顯示i的值,第一次為1,第二次為2,第三次為3,以此類推。
其實,這段程式碼就建立了一個閉包。因為函式a()外的變數c引用了函式a()內的函式b(),就是說:

當函式a()的內部函式b()被函式a()外的一個變數引用的時候,就建立了一個閉包。
1
在上面的例子中,由於閉包的存在使得函式a()返回後,a中的i始終存在,這樣每次執行c(),i都是自加1後的值。
從上面可以看出閉包的作用就是在a()執行完並返回後,閉包使得Javascript的垃圾回收機制GC不會收回a()所佔用的資源,因為a()的內部函式b()的執行需要依賴a()中的變數i。

在給定函式被多次呼叫的過程中,這些私有變數能夠保持其永續性。變數的作用域僅限於包含它們的函式,因此無法從其它程式程式碼部分進行訪問。不過,變數的生存期是可以很長,在一次函式呼叫期間所建立所生成的值在下次函式呼叫時仍然存在。正因為這一特點,閉包可以用來完成資訊隱藏,並進而應用於需要狀態表達的某些程式設計範型中。
1
下面來想象另一種情況,如果a()返回的不是函式b(),情況就完全不同了。因為a()執行完後,b()沒有被返回給a()的外界,只是被a()所引用,而此時a()也只會被b()引 用,因此函式a()和b()互相引用但又不被外界打擾(被外界引用),函式a和b就會被GC回收。所以直接呼叫a();是頁面並沒有資訊輸出。

下面來說閉包的另一要素引用環境。c()跟c2()引用的是不同的環境,在呼叫i++時修改的不是同一個i,因此兩次的輸出都是1。函式a()每進入一次,就形成了一個新的環境,對應的閉包中,函式都是同一個函式,環境卻是引用不同的環境。這和c()和c()的呼叫順序都是無關的。

以上就是對閉包作用的非常直白的描述,不專業也不嚴謹,但大概意思就是這樣,理解閉包需要循序漸進的過程。

Go的閉包

Go語言是支援閉包的,這裡只是簡單地講一下在Go語言中閉包是如何實現的。
下面我來講之前的JavaScript的閉包例子用Go來實現。

package main

import (
“fmt”
)

func a() func() int {
i := 0
b := func() int {
i++
fmt.Println(i)
return i
}
return b
}

func main() {
c := a()
c()
c()
c()

//a() //不會輸出i

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

可以發現,輸出和之前的JavaScript的程式碼是一致的。具體的原因和上面的也是一樣的,這裡就不在贅述了。
這頁說明Go語言是支援閉包的,至於具體是如何支援的目前就先做討論了。

關於閉包這裡也只講到了基本的概念,至於更深入的東西我目前能力有限只能靠以後慢慢摸索。就像上面講到的,理解閉包需要循序漸進的過程。

作者:minigeek
來源:CSDN
原文:https://blog.csdn.net/qq_22063697/article/details/74858264
版權宣告:本文為博主原創文章,轉載請附上博文連結!