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()
}
注意:
recover()
必須搭配defer
使用。defer
一定要在可能引發panic
的語句之前定義。