go語言中的函式支援普通函式、匿名函式、閉包函式、方法等
在go語言中函式是一等公民,函式可以作為引數、函式可以作為返回值、函式可以賦值給變數、函式可以作為閉包函式等
函式可以滿足介面
1、函式的定義
函式的基本定義方式如下:
func 函数名(参数列表) (返回值列表) {
函数体
}
引數列表:引數列表可以為空,也可以有多個引數,多個引數之間用逗號分隔
返回值列表:返回值列表可以為空,也可以有多個返回值,多個返回值之間用逗號分隔,返回值列表不為空時,函式中必須有return
舉個簡單的例子:
func add(a, b int, c float32) (int, error) {
return a + b, nil
}
在go語言中引數的傳遞時值傳遞,即傳遞的是值的副本,而不是值的引用,因此在函式中修改引數的值不會影響到原變數的值
但是關於切片的傳遞,由於切片的底層原理,在函式中修改切片的值會影響到原切片的值,因此在go語言中切片的傳遞需要注意。
當且切片發生擴容時,會返回一個新的切片,此時在函式中修改切片的值不會影響到原切片的值
除了上面講到的函式定義的方法,還可以在返回列表中執行返回引數的名稱(相當於定義一個引數名稱),這個引數在函式體中不需要再進行定義
下面是一個例子
func add(a, b int) (sum int, err error) {
sum = a + b
return sum, err
//也可以直接写return
}
2、函式的可變引數
函式在進行引數傳遞的時候沒可能不知道要傳多少個值,因此go語言中函式的引數列表中可以使用可變引數
可變引數的基本定義方式如下:
func 函数名(参数列表...类型) (返回值列表) {
函数体
}
舉一個簡單的例子:
func add(a ...int)(sum int, err error) {
for _, value := range a {
sum += value
}
}
c := 1
d := 2
sum, _ := add(a, b, 3, 4)
fmt.Println(sum)
其中a是一個int型別的切片,在函式中可以使用a[0]、a[1]等方式訪問切片中的元,也可以透過for迴圈進行訪問
3、函式一等公民特性
函式的一等公民特性,能夠當作引數進行傳遞、作為返回值進行返回、賦值給變數等操作
大大提升了函式的靈活性和複用性
3.1、將函式作為變數賦值
舉個例子:
func add(a ...int)(sum int, err error) {
for _, value := range a {
sum += value
}
}
func main(){
//这里将add函数作为参数赋值给变量funcVar
funcVar := add
c := 1
d := 2
sum, _ := funcVar(a, b, 3, 4)
fmt.Println(sum)
}
3.2、將函式作為返回值
舉個例子:
func cal(op string) func(){
switch op {
case "+":
return func(){
fmt.Println("这是加法")
}
case "-":
return func(){
fmt.Println("这是减法")
}
default:
return func(){
fmt.Println("这不是加法也不是减法")
}
}
}
//因为返回的是函数,所以在调用的时候需要加上()
cal("+")()
3.3、將函式作為引數進行傳遞
舉個例子(沒啥意義,只是說明用法):
func add(a, b int) int{
fmt.Printf("sum is %d\n", a + b)
}
func cal(y int, myfunc func(int, int)) {
myfunc(y, 2)
}
cal(1, add)
//输出结果为sum is 3
//调用cal函数传递add函数成为myfunc, 然后1+2
3.4、匿名函式
匿名函式就是沒有函式名的函式,匿名函式可以作為引數進行傳遞,也可以作為返回值進行返回
匿名函式是在傳遞或者返回的時候進行定義
//匿名函数作为参数进行传递
func add(a, b int) int{
fmt.Printf("sum is %d\n", a + b)
}
func cal(y int, myfunc func(int, int)) {
myfunc(y, 2)
}
cal(1, func(a, b int){
fmt.Printf("total is %d\n", a + b)
})
//输出结果为total is 3
//调用cal函数传递临时定义的匿名函数函数成为myfunc, 然后1+2
//匿名函数作为变量进行赋值
func cal(y int, myfunc func(int, int)) {
myfunc(y, 2)
}
localFunc := func(a, b int){
fmt.Printf("local is %d\n", a + b)
}
cal(1, localFunc)
//输出结果为local is 3
//调用cal函数传递localFunc成为myfunc, 然后1+2,跟上边的效果是一样的
4、go中函式的閉包特性
有一個需求,希望有一個函式每次呼叫返回的結果值都是增加一次之後的值
實現這個需求通常的方法是設定一個全域性變數,每次呼叫函式的時候,將全域性變數的值加1,然後返回全域性變數的值
全域性變數方式實現:
var local int
func add() int{
local += 1
return local
}
for i := 0; i < 5; i++ {
fmt.Println(add())
}
但是這種情況下會出現一個問題,想讓local歸零很難實現,而且被迫宣告瞭一個全集變數
因此go中設計了閉包的特性,下面是一個例子:
func auotoAdd() func() int {
local := 0
return func() int {
return local
}
}
nextFunc := auotoAdd()
for i := 0; i < 5; i++ {
fmt.Println(nextFunc())
}
//输出结果为0 1 2 3 4
//想要归零的话,只需要重新调用auotoAdd()函数即可
根據上面的例子可以得出閉包的定義:是指一個函式能夠訪問其外部作用域中的變數,即使外部函式已經結束執行。
比如:在一個函式中的匿名函式能夠訪問這個函式中的區域性變數,這個匿名函式稱為閉包。
5、defer的應用場景
defer可以理解為其它語言中的finally,在函式執行完畢之後執行,通常用於釋放資源、關閉檔案、關閉資料庫連線等操作
連線資料庫、開啟檔案、開始鎖等場景下,無論執行是否成功都要記得進行關閉操作,否則會造成資源洩漏等問題
// 一个在Java中的例子
try {
// 可能会发生异常的代码
} catch (Exception e) {
// 异常处理代码
} finally {
// 无论是否发生异常,都会执行的代码
}
在Java中try和finally之間的距離可能很遠,容易忘記關閉資源,而在go中可以使用defer來解決這個問題
在go中defer的使用方式如下:
var mu sync.Mutex
mu.lock()
defer mu.unlock() //defer后面的代码会在return之前执行
defer語句成對出現,可以防止忘記關閉資源,機制更好用,程式碼更簡潔
defer fmt.Println("1")
defer fmt.Println("2")
defer fmt.Println("3")
fmt.Println("4")
//输出结果为4 3 2 1
defer的執行順序是先進後出的,類似於棧的概念
defer的應用可以修改返回值
// 在这个例子中,defer 推迟执行的是一个匿名函数
// deferReturn 函数被调用,准备返回值 10。
// 在返回之前,defer 推迟执行的匿名函数被执行。
// 匿名函数中,result 被增加 1,因此 result 的值从 10 变为 11。
// deferReturn 函数返回修改后的 result 值,即 11。
// 因此,尽管 deferReturn 函数的返回语句是 return 10,但由于 defer 推迟执行的函数在返回之前修改了 result 的值,所以最终的输出结果是 11。这也展示了在 Go 语言中,defer 可以修改函数的返回值。
// 搞不懂就看源码
func deferReturn() (result int){
defer func(){
result++
}()
return 10
}
ret := deferReturn()
fmt.Println(ret) //输出结果为11
6、go的error、recover和panic
在go語言中關於出錯處理最重要的概念有三個:
1、error
2、panic
3、recover
go中關於語言錯誤處理的理念:不應該使用異常處理控制流程,而應該使用錯誤值。
其他語言在使用異常處理控制流程時,通常會使用try catch來捕獲異常,類似於包住函式 {: .prompt-tip }
在go語言中開發函式的人需要有一個返回值告訴呼叫者是否成功,go的設計者要求我們必須處理這個err,在go的程式碼中,會大量出現”if err != nil”這樣的語句
go的設計者認為必須處理這個err,繼續防禦性程式設計
所以go中使用panic來處理錯誤,panic會中斷當前的函式執行,然後在呼叫函式中查詢是否有recover,如果有recover則會執行recover,否則會一直向上查詢,直到找到recover或者程式崩潰
6.1、error
在go中error有專門的包,下面舉個例子:
import (
"fmt"
"errors"
)
func A() (int, error) {
return 1, errors.New("这是一个错误")
}
func main() {
if _, err := A(); err!= nil {
fmt.Println(err)
}
}
6.2、panic
panic是go中的內建函式,這個函式會導致程式退出,使用的場景不是很多
再舉個例子:
import (
"fmt"
"errors"
)
func A() (int, error) {
panic("this is a panic")
fmt.Println("能执行到这吗?")
return 1, errors.New("这是一个错误")
}
func main() {
if _, err := A(); err!= nil {
fmt.Println(err)
}
}
// 在上面的例子中,panic会导致程序退出,所以fmt.Println("能执行到这吗?")不会被执行,只会打印错误栈
panic會導致程式的退出,注意平時開發中不能隨便使用,下面是它的一些應用場景:
在一個服務啟動的過程中,開始必須要準備好一些依賴服務,日誌檔案是否存在、資料庫是否能連線、配置檔案有沒有問題等等,準備好後才能啟動服務
如果在進行服務啟動檢查的過程中,發現任何一項需求不能被滿足,就主動呼叫panic,讓程式退出
一旦伺服器啟動了,某行程式碼中不小心呼叫了panic,程式掛了就是非常嚴重的事故
在go中一些地方也會被動觸發panic:
1、陣列越界
2、空指標
……
為了能夠應對被動觸發panic的情況,go中提供了recover函式,recover函式可以捕獲panic,並且讓程式繼續執行,下一節介紹recover函式
panic後會返回一個interface{}型別的值,這個值就是panic的值
6.3、recover
下面是一個recover使用的例子:
// 在go中map的使用需要进行初始化,否则会报错,这是一个会被动触发panic的场景
func A() {
defer func(){
if err := recover(); err!= nil {
fmt.Println("recoverd is A(): ", err)
}
}()
var names map[string]string
names["xiaoming"] = "go工程师"
return 0, errors.New("这是一个错误")
}
// 上面的代码会输出recoverd is A(): assignment to entry nil map
還有一些使用的注意事項:
1、recover只能在defer函式中使用才能生效
2、defer需要在panic之前進行定義
3、recover處理異常後,邏輯並不會恢復到panic的點繼續執行
4、多個defer會形成棧,後定義的defer會先執行