日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

Go 语言学习笔记(二):函数

發布時間:2024/2/28 编程问答 30 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Go 语言学习笔记(二):函数 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

目錄

函數

函數定義

函數簽名和匿名函數

defer

閉包

? ? ? ? ?概念

panic 和 recover


函數

函數是程序執行的一個基本語法結構,Go 語言的很多特性是基于函數這個基礎實現的,比如命名類型的方法本質上是一個函數,類型方法是 Go 面向對象的實現基礎;接口底層同樣是通過指針和函數將接口和接口實例連接起來的。甚至 Go 并發語法糖 go 后面跟的也是函數。可見函數在 Go 中就是中流礫柱,既能起到"膠水"的作用,也為其他語言特性起到底層支撐的作用。

Go 不是一門純函數式的編程語言,但是函數在 Go 中是 "第一公民"?表現在 :

(1) 函數是一種類型,函數類型變量可以像其他類型變量一樣使用,可以作為其他函數的參數或返回值,也可以直接調用執行。

(2) 函數支持多值返回。

(3) 支持閉包。

(4) 函數支持可變參數

?

?

函數定義

函數是 Go 程序源代碼的基本構造單位,一個函數的定義包括如下幾個部分:函數聲明關鍵字 func、函數名、參數列表、返回列表和函數體。函數名遵循標識符的命名規則,首字母的大小寫決定該函數在其他包的可見性:大寫時其他包可見,小寫時只有相同的包可以訪問;函數的參數和返回值需要使用 " () " 包裹,如果只有一個返回值,而且使用的是非命名的參數 ,則返回參數的 " ()?" 可以省略。函數體使用 " {}?" 包裹,并且 "?{?" 必須位于函數返回值同行的行尾。

func funcName (param-list) (result- list) {function - body }

?

函數的特點

(1) 函數可以沒有輸入參數,也可以沒有返回值

(2) 多個相鄰的相同類型的參數可以使用簡寫模式。

func add( a,b int)?int { ??// a int, b int 簡寫為 a , b intreturn a + b }

(3) 支持有名的返回值,參數名就相當于函數體內最外層的局部變量,命名返回值變量會被初始化為類型零值,最后的 return 可以不帶參數名直接返回。

//?sum 相當于函數內的局部變量,被初始化為零 func add (a, b int) (sum int) {sum = a + breturn ?? // return sum 的簡寫模式//?sum := a + b ???// 如果是 sum := a + b,則相當于新聲明一個 sum 變量命名返回變量 sum 覆蓋//?return sum ?????// 最后需要顯式地調用 return sum }

(4) 不支持默認值參數。

(5) 不支持函數重載。

(6) 不支持函數嵌套,嚴格地說是不支持命名函數的嵌套定義,但支持嵌套匿名函數

func add (a , b int) (sum int) {anonynous := func(x , y int) int {return x + y}return anonymous(a , b) }

?

多值返回

Go 函數支持多值返回,定義多值返回的返回參數列表時要使用?" () "?包裹,支持命名參數的返回。

func swap (a, b int) (int, int) {return b , a }

習慣用法:如果多值返回值有錯誤類型,則一般將錯誤類型作為最后一個返回值。

?

實參到形參的傳遞

Go 函數實參到形參的傳遞永遠是值拷貝,有時函數調用后實參指向的值發生了變化,那是因為參數傳遞的是指針值的拷貝,實參是一個指針變量,傳遞給形參的是這個指針變量的副本,二者指向同一地址,本質上參數傳遞仍然是值拷貝。例如:

package main import "fmt"func chvalue(a int) int {a = a + 1return a }func chpointer(a *int) {*a = *a + 1return }func main() {a := 10chvalue(a)fmt.Println(a)chpointer(&a)fmt.Println(a) }

?

不定參數

Go 函數支持不定數目的形式參數,不定參數聲明使用 param ... type 的語法格式。

函數的不定參數有如下幾個特點:

(1) 所有的不定參數類型必須是相同的。

(2) 不定參數必須是函數的最后一個參數。

(3) 不定參數名在函數體內相當于切片,對切片的操作同樣適合對不定參數的操作。例如:

func sum(arr ... int) (sum int) {for _, v := range arr { ???// 此時 arr 就相當于切片,可以使用 range 訪問sum += v}return }

(4) 切片可以作為參數傳遞給不定參數,切片名后要加上 " ... " 例如:

func sum(arr ... int) (sum int) {for _, v := range arr {sum += v}return }func main() {slice := []int{1, 2 , 3 , 4}array := [...]int {1?, 2 , 3 , 4}// 數組不可以作為實參傳遞給不定參數的函數sum (slice ...) }

(5) 形參為不定參數的函數和形參為切片的函數類型不相同。

?

?

函數簽名和匿名函數

函數簽名

函數類型又叫函數簽名,一個函數的類型就是函數定義首行去掉函數名、參數名和?{,可以使用?fmt.Printf 的?"%T" 格式化參數打印函數的類型。

package main improt "fmt"func add(a, b int) int {return a + b }func main() {fmt.Printf("%T\n", add) // func(int, int) int }

?

兩個函數類型相同的條件是:擁有相同的形參列表和返回值列表(列表元素的次序、個數和類型都相同),形參名可以不同。以下 2 個函數的函數類型完全一樣:

func add(a,b int) int { return a+b} func sub (x int, y int) (c int) { c=x- y ; return c }

可以使用 type 定義函數類型,函數類型變量可以作為函數的參數或返回值。

package main import "fmt"func add(a, b int) int {return a + b }func sub(a, b int) int {return a - b }type Op func(int, int) int // 定義一個函數類型,輸入的是兩個int類型,返回值是一個int類型func do(f Op, a, b int) int {return f(a, b) // 函數類型變量可以直接用來進行函數調用 }func main() {a := do(add, 1, 2)fmt.Println(a)s := do(sub, 1, 2)fmt.Println(s) }

函數類型和 map、slice、chan 一樣,實際函數類型變量和函數名都可以當作指針變量,該指針指向函數代碼的開始位置。通常說函數類型變量是一種引用類型,未初始化的函數類型的變量的默認值是 nil。

Go 中函數是?"第一公民"。有名函數的函數名可以看作函數類型的常量,可以直接使用函數名調用函數,也可以直接賦值給函數類型變量,后續通過該變量來調用該函數。

package main func sum(a, b int) int {return a + b }func main() {sum(3, 4) // 直接調用f := sum // 有名函數可以直接賦值給變量f(1, 2) }

?

匿名函數

Go 提供兩種函數:有名函數和匿名函數。匿名函數可以看作函數字面量,所有直接使用函數類型變量的地方都可以由匿名函數代替。名函數可以直接賦值給函數變量,可以當作實參,也可以作為返回值,還可以直接被調用。

package main import "fmt" // 匿名函數被直接賦值函數變量 var sum = func(a, b int) int {return a + b }func doinput(f func(int, int) int, a, b int) int {return f(a, b) } // 匿名函數作為返回值 func wrap(op string) func(int, int) int {switch op {case "add":return func(a, b int) int {return a + b}case "sub":return func(a, b int) int {return a - b}default:return nil} }func main() {// 匿名函數直接被調用defer func() {if err := recover(); err != nil {fmt.Println(err)}}()sum(1, 2)// 匿名函數作為實參doinput(func(x, y int) int {return x + y}, 1, 2)opFunc := wrap("add")re := opFunc(2, 3)fmt.Printf("%d\n", re) }

?

?

defer

Go 函數里提供了 defer 關鍵字,可以注冊多個延遲調用( defer 后面的函數在 defer 語句所在的函數執行結束的時候會被調用 ),這些調用以先進后出( FILO )的順序在函數返回前被執行。這有點類似于 Java 語言中異常處理中的 finaly 子句。 defer 常用于保證一些資源最終一定能夠得到回收和釋放。

package main func main() {defer func() {println("first")}()defer func() {println("second")}()println("function body") }輸出: function body second first

defer 后面必須是函數或方法的調用,不能是語句,否則會報 expression in defer must be function call 錯誤。

defer 函數的實參在注冊時通過值拷貝傳遞進去。下面示例代碼中,實參 a 的值在 defer 注冊時通過值拷貝傳遞進去,后續語句 a++ 并不會影響 defer 語句最后的輸出結果。

func f() int {a := 0defer func(i int) {println("defer i = ", i)}(a)a++return a }打印結果:defer i = 0

defer 語句必須先注冊后才能執行,如果 defer 位于 return 之后,則 defer 因為沒有注冊,不會執行。

package mainfunc main() {defer func() {println("first")}()a := 0println(a)return defer func() {println("second")}() }輸出:0

主動調用 os.Exit(int) 退出進程時, defer 將不再被執行(即使 defer 已經提前注冊) 。

package main import "os"func main() {defer func() {println("defer")}()println("func body")os.Exit(1) }輸出: func body exit status 1

?

defer 的好處是可以在一定程度上避免資源泄漏,特別是在有很多 return 語句,有多個資源需要關閉的場景中,很容易漏掉資源的關閉操作。例如:

func CopyFile(dst, src string) (w int64, err error) {src, err := os.Open(src)if err != nil {return }dst, err := os.Create(dst)if err != nil {src.Close() // src很容易忘記關閉return }w, err = io.Copy(dst, src)dst.Close()dst.Close()return }

使用 defer 改寫后,在打開資源無報錯后直接調用 defer 關閉資源,一旦養成這樣的編程習慣,則很難會忘記資源的釋放。例如:

func CopyFile(dst, src string) (w int64, err error) {src, err := os.Open(src)if err != nil {return }defer src.Close()dst, err := os.Create(dst)if err != nil {return}defer dst.Close()w, err = io.Copy(dst, src)return }

defer 語句的位置不當,有可能導致 panic ,一般 defer 語句放在錯誤檢查語句之后。defer 也有明顯的副作用:defer 會推遲資源的釋放,defer 盡量不要放到循環語句里面,將大函數內部的 defer 語句單獨拆分成一個小函數是一種很好的實踐方式。另外,defer 相對于普通的函數調用需要間接的數據結構的支持,相對于普通函數調用有一定的性能損耗。

?

?

閉包

概念

閉包是由函數及其相關引用環境組合而成的實體,一般通過在匿名函數中引用外部函數的局部變量或包全局變量構成。

閉包 = 函數 + 引用環境

閉包對閉包外的環境引入是直接引用,編譯器檢測到閉包,會將閉包引用的外部變量分配到堆上。

如果函數返回的閉包引用了該函數的局部變量(參數或函數內部變量):

(1) 多次調用該函數,返回的多個閉包所引用的外部變量是多個副本,原因是每次調用函數都會為局部變量分配內存 。

(2) 用一個閉包函數多次,如果該閉包修改了其引用的外部變量,則每一次調用該閉包對該外部變量都有影響,因為閉包函數共享外部引用。

package mainfunc fa(a int) func(i int) int {return func(i, int) int {println(&a, a)a = a + ireturn a} }func main() {f := fa(1) // f 引用的外部的閉包環境包括本次函數調用的形參 a 的值 1g := fa(1) // g 引用的外部的閉包環境包括本次函數調用的形參 a 的值 1// 此時 f、g 引用的閉包環境中的 a 的值并不是同一個,而是兩次函數調用產生的副本println(f(1))// 多次調用 f 引用的是同一個副本 aprintln(f(1))// g 中 a 的值仍然是 1println(g(1))println(g(1)) }

f 和 g 引用的是不同的 a。如果一個函數調用返回的閉包引用修改了全局變量,則每次調用都會影響全局變量。如果函數返回的閉包引用的是全局變量 a,則多次調用該函數返回的多個閉包引用的都是同一個 a 。同理,調用一個閉包多次引用的也是同一個 a。此時如果閉包中修改了 a 值的邏輯,則每次閉包調用都會影響全局變量 a 的值。使用閉包是為了減少全局變量,所以閉包引用全局變量不是好的編程方式。

package mainvar (a = 0 )func fa() func(i, int) int {return func(i int) int {println(&a, a)a = a + ireturn a} }func main() {f := fa() // f 引用的外部的閉包環境包括全局變量 ag := fa() // f 引用的外部的閉包環境包括全局變量 a// 此時,f、g 引用的閉包環境中的 a 的值是同一個println(f(1)) println(g(1))println(g(1))println(g(1)) }

?

同一個函數返回的多個閉包共享該函數的局部變量。例如:

package main func fa(base int) (func(int) int, func(int) int) {println(&base, base)add := func(i int) int {base += iprintln(&base, base)return base}sub := func(i int) int {base -= iprintln(&base, base)return base}return add, sub }func main() {// f、g 閉包引用的 base 是同一個, 是 fa 函數調用傳遞過來的實參值f, g := fa(0) // s、k 閉包引用的 base 是同一個, 是 fa 函數調用傳遞過來的實參值s, k := fa(0)// f、g 和 s、k 引用不同的閉包變量,這是由于 fa 每次調用都要重新分配形參println(f(1), g(2))println(s(1), k(2)) }

?

閉包的價值

閉包最初的目的是減少全局變量,在函數調用的過程中隱式地傳遞共享變量,有其有用的一面;但是這種隱秘的共享變量的方式帶來的壞處是不夠直接,不夠清晰,除非是非常有價值的地方,一般不建議使用閉包。

對象是附有行為的數據,而閉包是附有數據的行為,類在定義時已經顯式地集中定義了行為,但是閉包中的數據沒有顯式地集中聲明的地方,這種數據和行為耦合的模型不是一種推薦的編程模型,閉包僅僅是錦上添花的東西,不是不可缺少的。

?

?

panic 和 recover

panic 和 recover 兩個內置函數用來處理 Go 的運行時錯誤( runtime errors )。panic 用來主動拋出錯誤,recover 用來捕獲 panic 拋出的錯誤。

基本概念

panic 和 recover 的函數簽名如下:

panic(i?interface{}) revover()interface{}

引發 panic 有兩種情況,一種是程序主動調用 panic 函數,另一種是程序產生運行時錯誤,由運行時檢測井拋出。

發生 panic 后,程序會從調用 panic 的函數位置或發生 panic 的地方立即返回,逐層向上執行函數的 defer 語句,然后逐層打印函數調用堆棧,直到被 recover 捕獲或運行到最外層函數而退出。

panic 的參數是一個空接口類型 interface{},所以任意類型的變量都可以傳遞給 panic。調用 panic 的方法非常簡單:panic(xxx

panic 不但可以在函數正常流程中拋出,在 defer 邏輯里也可以再次調用 panic 或拋出 panicdefer 里面的 panic 能夠被后續執行的 defer 捕獲。

?

recover() 用來捕獲 panic,阻止 panic 繼續向上傳遞。recover() 和 defer 一起使用,但是 recover()?只有在 defer 后面的函數體內被直接調用才能捕獲 panic 終止異常,否則返回 nil,異常繼續向外傳遞。

// 這個會捕獲失敗 defer recover()// 這個會捕獲失敗 defer fmt.Println(recover())// 這個嵌套兩層也會捕獲失敗 defer func() {func() {println("defer inner")recover() //無效}() }()// 如下場景會捕獲成功 defer func() {println("defer inner" )recover() }() func except() {recover() } func test() {defer except()panic("test panic") }

可以有連續多個 panic 被拋出,連續多個 panic 的場景只能出現在延遲調用里面,否則不會出現多個 panic 被拋出的場景。但只有最后一次 panic 能被捕獲。例如:

panic package main import "fmt"func main() {defer func() {if err := recover(); err != nil {fmt.Println(err)}}()// 只有最后一次 panic 調用能夠捕獲defer func() {panic("first defer panic")}()defer func() {panic("second defer panic")}()panic("main body panic") }

?

包中 init 函數引發的 panic 只能在 init 函數中捕獲,在 main 中無法被捕獲,原因是 init 函數先于 main 執行。函數并不能捕獲內部新啟動的 goroutine 所拋出的 panic。例如:

package main import ("fmt""time" )func do() {// 這里并不能捕獲 da 函數中的 panicdefer func() {if err := recover(); err != nil {fmt.Println(err)}}()go da()go db()time.Sleep(3 * time.Second) }func da() {panic("panic da")for i := 0; i < 10; i++ {fmt.Println(i)} }func db() {for i:= 0; i < 10; i++ {fmt.Println(i)} }

?

使用場景一般有兩種情況:

(1) 程序遇到了無法正常執行下去的錯誤,主動調用 panic 函數結束程序運行 。

(2) 在調試程序時,通過主動調用 panic 實現快速退出,panic 打印出的堆棧能夠更快地定位錯誤。為了保證程序的健壯性,需要主動在程序的分支流程上使用 recover() 攔截運行時錯誤。

Go 提供了兩種處理錯誤的方式,一種是借助 panic 和 recover 的拋出捕獲機制,另一種是使用 error 錯誤類型。

總結

以上是生活随笔為你收集整理的Go 语言学习笔记(二):函数的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。