Go 函數整理
函數是一塊執行特定任務的代碼。一個函數是在輸入源基礎上,通過執行一系列的算法,生成預期的輸出。
Go 裏面有三種類型的函數:
-
普通的帶有名字的函數
-
匿名函數
-
方法(Methods)
1. 普通函數
func functionName(parameter_list) (return_value_list) {
…
}
//parameter_list 的形式爲 (param1 type1, param2 type2, …)
//return_value_list 的形式爲 (ret1 type1, ret2 type2, …)
函數的聲明以關鍵詞 func
開始,後面緊跟自定義的函數名 functionname (函數名)
。函數能夠接收參數供自己使用,也可以返回零個或多個值(我們通常把返回多個值稱爲返回一組值)。
1.1 函數特性
1.1.1 多返回值
多值返回是 Go 的一大特性,爲我們判斷一個函數是否正常執行提供了方便。
func vals() (int, error) {
return 3, nil
}
func main() {
// 獲取函數的兩個返回值
a, err := vals()
if err != nil {
fmt.Println(a)
}
}
1.1.2 空白符
空白符用來匹配一些不需要的值,然後丟棄掉。
func vals() (int, error) {
return 3, nil
}
func main() {
// 如果你只對多個返回值裏面的幾個感興趣
// 可以使用下劃線(_)來忽略其他的返回值
c, _ := vals()
fmt.Println(c) //3
}
1.1.3 變長參數
如果函數的最後一個參數是採用 ...type
的形式,那麼這個函數就可以處理一個變長的參數,這時函數可以接受任意個 type
類型參數作爲最後一個參數。需要注意只有函數的最後一個參數才允許是可變的。
func find(num int, nums ...int) {
fmt.Printf("type of nums is %T\n", nums)
found := false
for i, v := range nums {
if v == num {
fmt.Println(num, "found at index", i, "in", nums)
found = true
}
}
if !found {
fmt.Println(num, "not found in ", nums)
}
fmt.Printf("\n")
}
func main() {
find(89, 89, 90, 95) //type of nums is []int 89 found at index 0 in [89 90 95]
find(78, 38, 56, 98) //type of nums is []int 78 not found in [38 56 98]
find(87) //type of nums is []int 87 not found in []
}
可變參數函數的工作原理是把可變參數轉換爲一個新的切片。
func find(num int, nums ...int) {
fmt.Printf("type of nums is %T\n", nums)
found := false
for i, v := range nums {
if v == num {
fmt.Println(num, "found at index", i, "in", nums)
found = true
}
}
if !found {
fmt.Println(num, "not found in ", nums)
}
fmt.Printf("\n")
}
func main() {
nums := []int{89, 90, 95}
find(89, nums)
}
上面的例子中我們將一個切片傳給一個可變參數函數。這種情況下無法通過編譯,編譯器報出錯誤 cannot use nums (type []int) as type int in argument to find
。原因是在這裏 nums
已經是一個 int 類型切片,編譯器試圖在 nums
基礎上再創建一個切片,所以失敗。
有一個可以直接將切片傳入可變參數函數的語法糖,你可以在在切片後加上 ...
後綴。如果這樣做,切片將直接傳入函數,不再創建新的切片。
func main() {
nums := []int{89, 90, 95}
find(89, nums...) //type of nums is []int 89 found at index 0 in [89 90 95]
}
如果一個變長參數的類型沒有被指定,則可以使用默認的空接口 interface{}
,這樣就可以接受任何類型的參數。該方法不僅可以用於長度未知的參數,還可以用於任何不確定類型的參數。一般而言我們會使用一個 for-range 循環以及 switch 結構對每個參數的類型進行判斷:
func typecheck(..,..,values ...interface{}) {
for _, value := range values {
switch v := value.(type) {
case int: …
case float64: …
case string: …
case bool: …
default: …
}
}
}
1.1.4 函數重載
函數重載(function overloading)指的是可以編寫多個同名函數,只要它們擁有不同的形參與 / 或者不同的返回值,在 Go 裏面函數重載是不被允許的。這將導致一個編譯錯誤:
funcName redeclared in this book, previous declaration at lineno
1.1.5 函數參數傳遞
Go 語言中函數的傳參都是值傳遞(傳值),都是一個副本,一個拷貝。因爲拷貝的內容有時候是非引用類型(int、string、struct 等這些),這樣就在函數中就無法修改原內容數據;有的是引用類型(指針、map、slice、chan 等這些),這樣就可以修改原內容數據。
func main() {
var args int64= 1
fmt.Printf("實際參數的地址 %p\n", &args) //實際參數的地址 0xc00006e090
modifiedNumber(args) // args就是實際參數
fmt.Printf("改動後的值是 %d\n",args) //改動後的值是 1
}
func modifiedNumber(args int64) { //這裏定義的args就是形式參數
fmt.Printf("形參地址 %p \n",&args) //形參地址 0xc00006e098
args = 10
}
func main() {
var args = []int64{1,2,3}
fmt.Printf("切片args的地址: %p \n",args) //切片args的地址: 0xc00006c120
fmt.Printf("切片args第一個元素的地址: %p \n",&args[0]) //切片args第一個元素的地址: 0xc00006c120
fmt.Printf("直接對切片args取地址:%p \n",&args) //直接對切片args取地址:0xc000064440
modifiedNumber(args)
fmt.Println(args) //[10 2 3]
}
func modifiedNumber(args []int64) {
fmt.Printf("形參切片的地址 %p \n",args) //形參切片的地址 0xc00006c120
fmt.Printf("形參切片args第一個元素的地址: %p \n",&args[0]) //形參切片args第一個元素的地址: 0xc00006c120
fmt.Printf("直接對形參切片args取地址:%p \n",&args) //直接對形參切片args取地址:0xc0000644a0
args[0] = 10
}
1.1.6 defer
關鍵字 defer 允許我們推遲到函數返回之前(或任意位置執行 return
語句之後)一刻才執行某個語句或函數。
func a() {
i := 0
defer fmt.Println(i)
i++
return
}
func main() {
a() //0
}
當一個函數內多次調用 defer
時,Go 會把 defer
調用放入到一個棧中,隨後按照後進先出(Last In First Out, LIFO)的順序執行。
func f() {
for i := 0; i < 5; i++ {
defer fmt.Printf("%d ", i)
}
}
func main() {
f() //4 3 2 1 0
}
defer 的作用:
-
簡化資源回收:在函數中, 經常需要創建資源 (比如: 數據庫連接、文件句柄、鎖等) , 爲了在函數執行完畢後, 及時的釋放資源;
-
panic 異常的捕獲
-
對返回值進行操作
1.關閉文件流
// open a file
defer file.Close()
2.解鎖一個加鎖的資源
mu.Lock()
defer mu.Unlock()
3.關閉數據庫鏈接
// open a database connection
defer disconnectFromDB()
4.panic異常捕獲
func f() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered in f", r)
}
}()
fmt.Println("Calling g.")
g()
fmt.Println("Returned normally from g.")
}
5.對函數return的返回值進行操作
func func1(s string) (n int, err error) {
defer func() {
log.Printf("func1(%q) = %d, %v", s, n, err)
}()
return 7, io.EOF
}
func main() {
func1("Go") //2021/04/13 15:15:24 func1("Go") = 7, EOF
}
1.2 init 函數
init() 函數會在每個包完成初始化後自動執行,並且執行優先級比 main 函數高。init 函數通常被用來:
-
對變量進行初始化
-
檢查 / 修復程序的狀態
-
註冊
package main
import "fmt"
var _ int64=s()
func init(){
fmt.Println("init function --->")
}
func s() int64{
fmt.Println("function s() --->")
return 1
}
func main(){
fmt.Println("main --->")
}
執行結果如下:
function s() --->
init function --->
main --->
init() 函數特性:
-
每個包可以擁有多個 init 函數
-
包的每個源文件也可以擁有多個 init 函數,順序執行
-
不同包的 init 函數按照包導入的依賴關係決定該初始化函數的執行順序
-
init 函數不能被其他函數調用,而是在 main 函數執行之前,自動被調用
func init(){
fmt.Println("init 1")
}
func init(){
fmt.Println("init2")
}
func main(){
fmt.Println("main")
}
/*執行結果:
init1
init2
main */
1.3 內置函數
Go 語言擁有一些不需要進行導入操作就可以使用的內置函數。
2. 匿名函數
當我們不希望給函數起名字的時候,可以使用匿名函數。匿名函數即沒有名稱的函數。Go 語言支持匿名函數是因爲 Go 語言支持頭等函數的機制。支持頭等函數(First Class Function)的編程語言,可以把函數賦值給變量,也可以把函數作爲其它函數的參數或者返回值。
func main() {
a := func() {
fmt.Println("hello world first class function")
}
a()
fmt.Printf("%T", a)
}
在上面的程序中,我們將一個函數賦值給了變量 a
。這是把函數賦值給變量的語法。可以看到賦值給 a
的函數沒有名稱,由於沒有名稱,這類函數稱爲匿名函數(Anonymous Function)。
2.1 匿名函數特性
要調用一個匿名函數,可以不用賦值給變量。就像其它函數一樣,還可以向匿名函數傳遞參數。
func main() {
func(n string) {
fmt.Println("Welcome", n) //Welcome Gophers
}("Gophers")
}
2.2 自定義的函數類型
正如我們定義自己的結構體類型一樣,我們可以定義自己的函數類型。
type add func(a int, b int) int
func main() {
var a add = func(a int, b int) int {
return a + b
}
s := a(5, 6)
fmt.Println("Sum", s) //Sum 11
}
以上代碼片段創建了一個新的函數類型 add
,它接收兩個整型參數,並返回一個整型。
然後向它賦值了一個符合 add
類型簽名的函數。
2.3 將函數作爲參數
函數可以作爲其它函數的參數進行傳遞,然後在其它函數內調用執行
func simple(a func(a, b int) int) {
fmt.Println(a(60, 7))
}
func main() {
f := func(a, b int) int {
return a + b
}
simple(f) //67
}
2.4 將函數作爲返回值
函數可以作爲其它函數的返回值進行傳遞
func simple() func(a, b int) int {
f := func(a, b int) int {
return a + b
}
return f
}
func main() {
s := simple()
fmt.Println(s(60, 7)) //67
}
在上面程序中, simple
函數返回了一個函數,並接受兩個 int
參數,返回一個 int
。
我們調用了 simple
函數,並把 simple
的返回值賦值給了 s
。現在 s
包含了 simple
函數返回的函數。我們調用了 s
,並向它傳遞了兩個 int 參數,程序輸出 67。
2.5 閉包
閉包是由函數及其相關的引用環境組合而成的實體 (即:閉包 = 函數 + 引用環境)。在函數式語言中,當內嵌函數體內引用到體外的變量時,將會把定義時涉及到的引用環境和函數體打包成一個整體(閉包)。
在 Go 語言中,匿名函數就是一個閉包,它可以直接引用外部函數的局部變量。
func main() {
a := 5
func() {
fmt.Println("a =", a)
}()
}
在上面的程序中,匿名函數訪問了變量 a
,而 a
存在於函數體的外部。因此這個匿名函數就是閉包。
每一個閉包都會綁定它自己的外圍變量(Surrounding Variable)。
func appendStr() func(string) string {
t := "Hello"
c := func(b string) string {
t = t + " " + b
return t
}
return c
}
func main() {
a := appendStr()
b := appendStr()
fmt.Println(a("World")) //Hello World
fmt.Println(b("Everyone")) //Hello Everyone
fmt.Println(a("Gopher")) //Hello World Gopher
fmt.Println(b("!")) //Hello Everyone !
}
在上面程序中,函數 appendStr
返回了一個閉包。這個閉包綁定了變量 t
。
我們首先用參數 World
調用了 a
。現在 a
中 t
值變爲了 Hello World
。然後我們又用參數 Everyone
調用了 b
。由於 b
綁定了自己的變量 t
,因此 b
中的 t
還是等於初始值 Hello
。於是該函數調用之後,b
中的 t
變爲了 Hello Everyone
。
閉包的使用
package main
import (
"fmt"
)
type student struct {
firstName string
lastName string
grade string
country string
}
func filter(s []student, f func(student) bool) []student {
var r []student
for _, v := range s {
if f(v) == true {
r = append(r, v)
}
}
return r
}
func main() {
s1 := student{
firstName: "Naveen",
lastName: "Ramanathan",
grade: "A",
country: "India",
}
s2 := student{
firstName: "Samuel",
lastName: "Johnson",
grade: "B",
country: "USA",
}
s := []student{s1, s2}
f := filter(s, func(s student) bool {
if s.grade == "B" {
return true
}
return false
})
fmt.Println(f) //[{Samuel Johnson B USA}]
}
在上面的代碼中,filter
的第二個參數是一個函數。這個函數接收 student
參數,返回一個 bool
值。這個函數計算了某一學生是否滿足篩選條件。我們遍歷了 student
切片,將每個學生作爲參數傳遞給了函數 f
。如果該函數返回 true
,就表示該學生通過了篩選條件,接着將該學生添加到了結果切片 r
中。
在 main
函數中,我們首先創建了兩個學生 s1
和 s2
,並將他們添加到了切片 s
。現在假設我們想要查詢所有成績爲 B
的學生。爲了實現這樣的功能,我們傳遞了一個檢查學生成績是否爲 B
的函數,如果是,該函數會返回 true
。我們把這個函數作爲參數傳遞給了 filter
函數。上述程序會輸出:
[{Samuel Johnson B USA}]
假設我們想要查找所有來自印度的學生。通過修改傳遞給 filter
的函數參數,就很容易地實現了。
c := filter(s, func(s student) bool {
if s.country == "India" {
return true
}
return false
})
fmt.Println(c) //[{Naveen Ramanathan A India}]
3. 方法
在 func
這個關鍵字和方法名中間加入了一個特殊的接收器類型。接收器可以是結構體類型或者是非結構體類型。接收器是可以在方法的內部訪問的。
func (t Type) methodName(parameter_list) (return_value_list) {
}
3.1 使用示例
package main
import (
"fmt"
)
type Employee struct {
name string
salary int
currency string
}
/*
displaySalary() 方法將 Employee 做爲接收器類型
*/
func (e Employee) displaySalary() {
fmt.Printf("Salary of %s is %s%d \n", e.name, e.currency, e.salary)
}
/*
displaySalary()方法被轉化爲一個函數,把 Employee 當做參數傳入。
*/
func displaySalary(e Employee) {
fmt.Printf("Salary of %s is %s%d \n", e.name, e.currency, e.salary)
}
func main() {
emp1 := Employee {
name: "Sam Adolf",
salary: 5000,
currency: "$",
}
emp1.displaySalary() //調用方法 Salary of Sam Adolf is $5000
displaySalary(emp1) //調用函數 Salary of Sam Adolf is $5000
}
3.2 使用原因
Go 不是純粹的面向對象編程語言,而且 Go 不支持類。因此,基於類型的方法是一種實現和類相似行爲的途徑。
相同的名字的方法可以定義在不同的類型上,而相同名字的函數是不被允許的。
package main
import (
"fmt"
"math"
)
type Rectangle struct {
length int
width int
}
type Circle struct {
radius float64
}
func (r Rectangle) Area() int {
return r.length * r.width
}
func (c Circle) Area() float64 {
return math.Pi * c.radius * c.radius
}
func main() {
r := Rectangle{
length: 10,
width: 5,
}
fmt.Printf("Area of rectangle %d\n", r.Area())
c := Circle{
radius: 12,
}
fmt.Printf("Area of circle %f", c.Area())
}
3.3 指針接收器與值接收器
package main
import (
"fmt"
)
type Employee struct {
name string
age int
}
/*
使用值接收器的方法。
*/
func (e Employee) changeName(newName string) {
e.name = newName
}
/*
使用指針接收器的方法。
*/
func (e *Employee) changeAge(newAge int) {
e.age = newAge
}
func main() {
e := Employee{
name: "Mark Andrew",
age: 50,
}
fmt.Printf("Employee name before change: %s", e.name) //Employee name before change: Mark Andrew
e.changeName("Michael Andrew")
fmt.Printf("\nEmployee name after change: %s", e.name) //Employee name after change: Mark Andrew
fmt.Printf("\n\nEmployee age before change: %d", e.age) //Employee age before change: 50
(&e).changeAge(51)
fmt.Printf("\nEmployee age after change: %d", e.age) //Employee age after change: 51
e.changeAge(52)
fmt.Printf("\nEmployee age after change: %d", e.age) //Employee age after change: 52
}
在上面的程序中,changeName
方法有一個值接收器 (e Employee)
,而 changeAge
方法有一個指針接收器 (e *Employee)
。在 changeName
方法中對 Employee
結構體的字段 name
所做的改變對調用者是不可見的,因此程序在調用 e.changeName("Michael Andrew")
這個方法的前後打印出相同的名字。由於 changeAge
方法是使用指針 (e *Employee)
接收器的,所以在調用 (&e).changeAge(51)
方法對 age
字段做出的改變對調用者將是可見的。我們使用 (&e).changeAge(51)
來調用 changeAge
方法。由於 changeAge
方法有一個指針接收器,所以我們使用 (&e)
來調用這個方法。其實沒有這個必要,Go 語言讓我們可以直接使用 e.changeAge(51)
。e.changeAge(51)
會自動被 Go 語言解釋爲 (&e).changeAge(51)
。
一般來說,指針接收器可以使用在:
-
對方法內部的接收器所做的改變應該對調用者可見時。
-
當拷貝一個結構體的代價過於昂貴時。考慮下一個結構體有很多的字段。在方法內使用這個結構體做爲值接收器需要拷貝整個結構體,這是很昂貴的。在這種情況下使用指針接收器,結構體不會被拷貝,只會傳遞一個指針到方法內部使用。
3.4 非結構體上的方法
爲了在一個類型上定義一個方法,方法的接收器類型定義和方法的定義應該在同一個包中。
package main
func (a int) add(b int) {
}
func main() {
}
在上面程序中,我們嘗試把一個 add
方法添加到內置的類型 int
。這是不允許的,因爲 add
方法的定義和 int
類型的定義不在同一個包中。該程序會拋出編譯錯誤 cannot define new methods on non-local type int
。
我們可以爲內置類型 int 創建一個類型別名,然後創建一個以該類型別名爲接收器的方法。
package main
import "fmt"
type myInt int
func (a myInt) add(b myInt) myInt {
return a + b
}
func main() {
num1 := myInt(5)
num2 := myInt(10)
sum := num1.add(num2)
fmt.Println("Sum is", sum)
}
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/lkVbZ1-ps4BzloveQb2ceA