Golang的函數(func)


前言

Go中對函數的使用非常普遍,Go語言中沒有默認參數這個概念。

 

 

函數格式

func 函數名(參數1,參數2,......)(返回值1,返回值2,....){   }

 

package main

import (
	"fmt"
)

//函數

//函數的定義:Go是強類型語言必須給 參數、和返回值指定數據類型
//返回值使用和參數各使用()分開
func sum(x1 int, x2 int) (ret int) {
	return x1 + x2

}

//沒有返回值的函數
func f1(x1 int, x2 int) {
	println(x1 + x2)
}

//沒有參數的函數,也沒有返回值的函數
func f3() {
	fmt.Println("hello world")

}

//沒有參數,但是有返回值的函數
func f4() string {
	return "Hello World"
}

//函數的返回值可以命名也不可不命名
func f5(x int, y int) (ret int) {
	ret = x + y
	return //使用命名返回值,為ret,return后面可省略

}

//返回多個返回值
func f6() (int, string) {
	return 1, "2"
}

//參數的類型簡寫:當函數有多個參數並且類型一致時,我們可以簡寫前面的參數類型
func f7(x, y, z int, n, o string, i, j bool) int {
	return x + y

}

//可變長的參數:可變長參數必須放在最后
func f8(x string,y...int){
	fmt.Println(x)
	fmt.Println(y) //切片類型[1 2 3]
}



func main() {
	ret := sum(1, 1)
	fmt.Println(ret)
	f1(2, 3)
	str01 := f4()
	fmt.Println(str01)
	n, s := f6()
	fmt.Println(n, s)
	f8("下雨了",1,2,3)

}

 

函數參數

在Go語言中函數參數 通過 值類型傳遞!也就是說Go函數中的參數都是副本!

In Go, the function parameters are passed by value.
With respect to use slice as a function argument, that means the function will get the copies of the slice: a pointer which points to the starting address of the underlying array, accompanied by the length and capacity of the slice.
Oh boy! Since you know the address of the memory which is used to store the data, you can weak the slice now. Let's see the following example:
 
Go語言中函數中的參數傳遞的是值類型
至於/關於(with respect to...)使用切片作為函數參數,意味着該函數將會得到1個該切片的副本 - 該副本切片指向了該切片的底層數組的起始位置。
切片中有什么元素由該切片指向的底層數組決定,  2個切片指向了同1個底層數組不同的位置,2個切片中如果有1個修改了這個底層數組同1個位置,會影響另1個切片的元素,如果是追加則不會。
package main

import "fmt"

var s1 []string

func modify(s1 []string) {
	s1[0] = "Zero" //修改了s1副本和s1本身對應的底層數組的同1個位置
}

func expansion(s2 []string) {
	s2 = append(s2, "3")//擴展了、不算修改底層數組原處!
	fmt.Println("我append數據了啊:",s2)

}

func main() {
	s1 = []string{"1", "2"}
	s1[0] = "一"
	fmt.Println(s1)
	modify(s1)
	fmt.Println(s1)
	expansion(s1)
	fmt.Println("增加了沒有",s1)

}

  

可變參數

可變長的參數 

package main

import "fmt"

func f1(args ...interface{}) {
	//args空接口類型的slice類型
	fmt.Printf("類型:%T,可變參數值:%v\n", args, args)
}

func main() {
	f1()
	f1(1)
	f1(2, 3, 4)
	//拆開傳值
	slice1 := []interface{}{5, 6, 7}
	f1(slice1...)
}

 

參數總結

//1.可選參數:調用f1函數是option可以不傳,傳參之后函數接收到1個slice。
func f1(option ...string) {
    fmt.Println(option)
}

//2.必須傳參:參數可以是任意類型,如果沒有參數可以傳nil
func f2(option interface{}) {
    fmt.Println(option)
}
//3.兼容以上2種情況option可選參數,參數可以是任意類型
func f3(option ...interface{})  {
    fmt.Println(option)
}

 

 

 

 

閉包函數

1個函數嵌套了另1個函數,內層的函數引用了外層函數的參數或者變量之后,這個外層函數就行包一樣包住了內層的函數,外層函數就叫閉包函數。

值得注意得是在Go的函數里面,你只能定義1個匿名函數,不能像Python 一樣在函數內部在聲明1個有名函數。

     Python versus Golang     

 

閉包的原理

1.函數A可以作為函數B的1個參數傳到函數B。

2.函數查找變量的順序為 首先在自己內部找,找不到再去外層函數找。

3.函數A也可以作為函數B的返回值返回。

package main

import "fmt"

//1個簡單的閉包函數  結束1個 int參數x,返回1個參數為int和返回值為int的函數

func adder(x int) func(int) int {
	//內部的匿名函數引用了外部adder函數的變量x就稱為閉包
	return func(y int) int {
		x += y
		return x
	}
}

func main() {
	ret := adder(10)
	fmt.Println(ret(100))
}

  

 

package main
import "fmt"

//1.需求在不改變CEO和CTO寫得代碼的情況下,把CTO寫得的函數傳到CEO函數中執行

//CEO寫得函數(代碼不能動)
func ceo(f func()) {
	fmt.Println("This is CEO 函數")
	f()
}

//CTO寫的函數
func cto(y...int) {
	var sum int
	for _,v:=range(y){
		sum+=v
	}
	fmt.Println("This is CTO 函數",sum)
}


//我寫得函數
func wraper(f func(...int), a ...int) func() {
	tmp := func() {
		f(a...)
	}
	fmt.Println("This is function code by me")
	return tmp
}

func main() {
	// //匿名函數
	// var f1 = func(x, y int) {
	// 	fmt.Println(x + y)
	// }
	// f1(10, 20)

	// //立即執行函數
	// func(x, y int) {
	// 	fmt.Println(x + y)

	// }(10, 200)
	//閉包
	w := wraper(cto, 2020, 4,4,10,41)
	ceo(w)

}

  

  

 

閉包面試題

package main

import "fmt"

func cal(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 := cal(10)
	//由於 內部函數一直引用外部函數的變量i,所以外部函數的變量i一直在變。
	fmt.Println(f1(1), f2(2)) //11 9
	fmt.Println(f1(3), f2(4)) //12 8
	fmt.Println(f1(5), f2(6)) //13 7

}

  

遞歸函數 

遞歸。遞 、 歸 所說的就是 遞和歸2個不同動作過程。有遞也有歸才叫遞歸。遞就是壓棧的過程,歸就是出棧的過程。

遞歸函數就是自己調用自己

應用場景:問題相同但是每次問題規模都減小

必須有1個退出條件:遞 、歸 。遞 、歸不能無限得傳遞(壓棧)達到了1臨界值(達到了目的) 就得歸(出棧)。

 

 

 

package main

import "fmt"
func test(n int){
	fmt.Println("--->n=",n)
	if n<3{
		n++
		test(n)//從這里進去的,就從這里出來
	}
	fmt.Println("<----n=",n)
	
}
func main() {
	test(1)
}


/*
main函數開始執行
test(1):棧(一)
	1.fmt.Println("--->n=",n) 打印:--->n= 1
	2.if n<3{n=1所以條件成立}n++之后 n=2
	3.遇到函數自己調用自己,開辟新棧(二)也就是test(2)
###############################################################	
	13.出棧二來到在棧一步驟3位置,代碼往下執行fmt.Println("<----n=",n)棧一中此時變量n=3啊,所以打印:<----n= 2
	14.main函數結束

test(2)	棧(二)
	4.fmt.Println("--->n=",n) 打印:--->n= 2
	5.if n<3{n=2所以條件成立}n++之后   n=3
	6.遇到函數自己調用自己,開辟新棧(三)也就是test(3)
###############################################################
	11.出棧三 來到在棧二步驟6的位置,代碼往下執行fmt.Println("<----n=",n)棧二中此時變量n=3啊,所以打印:<----n= 3	
	12.棧(一)步驟3給我壓棧的棧,我出棧就到棧(一)步驟3	

test(3)棧(三)
	7.fmt.Println("--->n=",n) 打印:--->n= 3
	8.if n<3{n=3所以條件不成立了} n++不執行 test()也不會調用自己進行壓棧
	9.不壓棧了就出棧,看到fmt.Println("<----n=",n)就執行所以打印:<----n= 3
	10.然后出棧,步驟6給我壓棧的棧,我出棧了就從步驟6 出去	


*/

  

文件夾&遞歸思想

遞歸思想對我們影響深遠,每天打開電腦進入文件夾、查找子文件、進入子文件 也算得上是遞歸操作

:進入1個目錄 ------>進入這個目錄的子目---------> 進入子目錄的子目錄......

臨界值:找到/看到自己想要的

:再逐步退出來

rm -rf /*  中的 -r 選項是什么意思?

-r, -R, --recursive remove directories and their contents recursively

import  os
file_info=[]
def recursion_files(inital_path="D:\goproject"):
    files = os.listdir(inital_path)
    if not  files:
        return
    for item in files:
        fileAbs = os.path.join(inital_path,item)
        if os.path.isfile(fileAbs):
            file_info.append(fileAbs)
        else:
            #如果有文件夾就把該文件夾下的子目錄保存進行壓棧操作
            recursion_files(inital_path=fileAbs)
recursion_files()
print(file_info)

  

 

 

階乘

package main
import "fmt"
func f(n int) int {
	if n <= 1 {
		return 1
	}
	return n*f(n-1)
}
func main() {
	ret:=f(5)
	fmt.Println(ret)
}

  

 有N個台階,1次可以走1步,也可以走2步,請問有多少種走法?

package main

import "fmt"

func f(n int) int {
	if n == 1 {
		return 1
	}
	if n == 2 {
		return 2
	}

	return f(n-1) + f(n-2)
}
func main() {
	ret := f(3)
	fmt.Println(ret)
}

  

錯誤處理recover()

Python中的異常、錯誤信息都屬於Exception類,都可以通過try except Exceptiong語句進行攔截捕捉到。

Golang中的錯誤信息屬於error 類型,它和panic是2種不同的東西

Golang中的錯誤信息是由於函數作為變量return出來的變量,而recover攔截的是Panic異常不是error類型的變量。所以我們寫了太多的if err!=nill{}。

recover函數可以幫助我們攔截到Golang程序執行過程中出現的panic,例如:引用了空指針/數組越界..然后進行現場的recover(恢復),確保程序不會崩潰無法繼續執行下面的代碼。

既然是攔截就一定需要在合理的位置和時機進行panic的攔截。

通常recover會和defer延遲執行結合使用,在程序執行之前進行defer注冊、在程序執行完畢之后 進行錯誤捕捉、攔截、處理。

var numberList []int
func main() {
    data := make(map[string]interface{}, 10)
    //defer+函數調用:在main函數結束之后再調用
    //注意:defer recover()一定要在錯誤發生之前進行注冊,可以未雨綢繆不能亡羊補牢!
    defer func() {
        //recover返回的是1個空接口類型的變量,我們可以對這個變量進行記錄!
        if p := recover(); p != nil {
            data["data"] = nil
            data["err_msg"] = p
            fmt.Println(data)
            //攔截可能出現的錯誤,返回給前端。
            marshal, err := json.Marshal(data)
            if err != nil {
                fmt.Println(err)
            }
            fmt.Println(marshal)
        }
    }()
    //模擬數組越界的錯誤!
    index := 10
    item := numberList[index]
    data["data"] = item

}

 

 

defer關鍵字

代碼大多情況下是按照由上至下的順序執行的,而Python中的 yeild、send(上下切換執行=協程)還有Golang中的defer關鍵字可以改變程序的執行順序

在Go的函數里可以聲明defer關鍵字+函數,有延遲調用函數的效果,多用於釋放sync.Mutex、file句柄、socket資源、數據庫事務操作

這是1個經典的例子我使用了defer+recover檢測事務執行過程中可能出現的err和panic再進行RollBACK。

//sqlx事務操作:transactionsn: Noun(一筆)交易,業務,買賣;辦理;處理
func sqxTransaction() (err error) {
    var transaction *sqlx.Tx
    transaction, err = db.Beginx() //開啟事務
    if err != nil {
        fmt.Println("開始事務失敗", err)
        return err
    }
    //defer延遲執行函數,最后檢查程序執行過程中是否出現panic?是否返回錯誤?
    defer func() {
        //程序執行出現panic之后回滾
        if p := recover(); p != nil {
            transaction.Rollback()
            panic(p)
            //程序執行返回錯誤之后回滾。
        } else if err != nil {
            fmt.Println("事務回滾:", err)
            transaction.Rollback()
            //程序執行過程沒出現panic、也沒有返回err信息,提交事務。
        } else {
            err = transaction.Commit()
            fmt.Println("事務執行成功")
        }
    }()
    //執行SQL1
    sqlStr1 := "update user set age=20 where id=?"
    ret, err := transaction.Exec(sqlStr1, 1)
    if err != nil {
        fmt.Println(err)
        return err
    }
    affectedRow, err := ret.RowsAffected()
    if err != nil {
        return err
    }
    if affectedRow != 1 {
        return errors.New("執行SQL1時未對數據庫進行修改!\n")
    }
    //執行SQL1成功之后開始執行SQL2
    sqlStr2 := "update user set age=19 where id=?"
    ret, err = transaction.Exec(sqlStr2, 2)
    if err != nil {
        return err
    }
    affectedRow, err = ret.RowsAffected()
    if err != nil {
        return err
    }
    if affectedRow != 1 {
        return errors.New("執行SQL2時未對數據庫進行修改!\n")
    }
    return err

}
sqlx事務操作

 

聲明1個defer 就是開辟1層獨立的棧針進行逐層壓棧操作在函數體內語句執行完畢之后,按照先進后出(后進先出)的順序出棧

 

defer關鍵字執行時機

Go語言的函數中return語句在底層並不是原子操作。

它分為2部執行:

1.返回值賦值

2.執行RET指令兩步。

defer語句執行的時機就在返回值賦值操作后,RET指令執行前

具體如下圖所示:

defer的執行步驟如下:

1.先執行函數中語句內容

2.遇到defer關鍵字 開辟獨立的defer棧空間(不同於函數)逐一壓棧(不執行)

3.給函數中return值=賦值

4.按先入后出的順序 執行defer棧中的語句內容

5.函數返回真正的返回值(執行ret指令)
 
 
 
package main

import "fmt"

func f1() int {
	x := 5
	defer func() {
		x++ //
	}()
	return x
}

func f2() (x int) {
	defer func() {
		x++
	}()
	return 5 //1.先給返回值賦值 x=5 2.defer壓棧x=5 3.執行x=5+1 4.return x=6
}

func f3() (y int) {
	x := 5
	defer func() { //壓棧時開辟了獨立的空間x=5 x+1
		x++
	}()
	return x //這里是x=y=5
}

func f4() (x int) {
	defer func(x int) {
		x++ //參數改寫的是副本
	}(x)
	return 5 //1.先給返回值賦值x=5 2.defer開辟獨立的棧 壓棧x=5 3.執行x++ 4.returm
}
func main() {
	fmt.Println(f1()) //5
	fmt.Println(f2()) //6
	fmt.Println(f3()) //5
	fmt.Println(f4()) //5
}

  

當defer的函數語句中遇到函數調用先執行函數然后再壓棧

defer func1("1", a, fun2("10", a, b)

defer壓棧時遇到函數調用,先把函數執行完畢之后,再執行壓棧。

 

package main

import "fmt"

func calc(index string, a, b int) int {
	ret := a + b
	fmt.Println(index, a, b, ret)
	return ret
}

func main() {

	a := 1
	b := 2
	//1.先執行calc("10", a, b)打印"10" 1 3 4
	//2.壓棧1 defer calc("1", a, 4)
	defer calc("1", a, calc("10", a, b))
	a=0
	//3.再執行calc("20", a, b)打印"20" 0 2 2
	//4.壓棧2:defer calc("2", 0, 2)
	defer calc("2", a, calc("20", a, b))
	//棧2出棧:2 0 2 2 
	//棧1出棧:1 1 3 4
	b=1
	

}

 

如何在嵌套函數內修改函數外部的變量?指針啊!

package main

import "fmt"

func outerFunc()(n int) {
    defer func(n *int ){
        *n+=10
        fmt.Println(*n)
    }(&n)
    n=900
    return n
}

func main() {
    n := outerFunc()
    fmt.Println(n)
}

 

defer里面再嵌套defer會是什么效果?遞歸嗎?

package main

import "fmt"

func f1() {
    fmt.Println("f1開始")
    defer func() {
        fmt.Println("f2開始")
        defer func() {
            fmt.Println("f3開始")
            defer func() {
                fmt.Println("f4開始")
                defer func() {
                    fmt.Println("f4結束")
                }()
            }()
            fmt.Println("f3結束")
        }()
        fmt.Println("f2結束")
    }()
    fmt.Println("f1結束")
}
func outerFunc() {
    fmt.Println("outerFunc開始")
    defer f1()
    fmt.Println("outerFunc結束")
}

func main() {
    outerFunc()
}
defer嵌套

 

defer、recover the panic

defer還有1個作用我們可以在程序執行結束之前,在defer的函數中執行recover捕捉到程序的panic進行處理,保證程序執行時遇到panic不會異常終止。

相當於Python里面的 Try Except功能。

func outerFunc() {
    //使用defer函數設置recover來捕捉panic
    defer func(){
        if p := recover(); p != nil {
            fmt.Println(p)
        }
    }()
    dsn := "zhanggen:ne3thd8p0s@tcp(192.168.56.18:3306)/web?charset=utf8mb4&parseTime=True"
    db,err:=sqlx.Connect("mysql", dsn)
    if err!=nil{
        //拋出err信息,recover就會接收到
        panic(err)
    }
    if db==nil{
        panic("數據庫連接未建立!")
    }

}

 

 

 

 

分金幣

你有50枚金幣,需要分配給以下幾個人:Matthew,Sarah,Augustus,Heidi,Emilie,Peter,Giana,Adriano,Aaron,Elizabeth。
分配規則如下:
a. 名字中每包含1個'e'或'E'分1枚金幣
b. 名字中每包含1個'i'或'I'分2枚金幣
c. 名字中每包含1個'o'或'O'分3枚金幣
d: 名字中每包含1個'u'或'U'分4枚金幣
寫一個程序,計算每個用戶分到多少金幣,以及最后剩余多少金幣?
程序結構如下,請實現 ‘dispatchCoin’ 函數
 
package main

import "fmt"

var (
	coins = 50
	users = []string{
		"Matthew", "Sarah", "Augustus", "Heidi", "Emilie", "Peter", "Giana", "Adriano", "Aaron", "Elizabeth",
	}
	distribution = make(map[string]int, len(users))
)

/*
你有50枚金幣,需要分配給以下幾個人:Matthew,Sarah,Augustus,Heidi,Emilie,Peter,Giana,Adriano,Aaron,Elizabeth。
分配規則如下:
a. 名字中每包含1個'e'或'E'分1枚金幣
b. 名字中每包含1個'i'或'I'分2枚金幣
c. 名字中每包含1個'o'或'O'分3枚金幣
d: 名字中每包含1個'u'或'U'分4枚金幣
寫一個程序,計算每個用戶分到多少金幣,以及最后剩余多少金幣?
程序結構如下,請實現 ‘dispatchCoin’ 函數
*/

func dispatchCoins() {
	for _, user := range users {
		for _, c := range user {
			switch c {
			case 'e', 'E':
				distribution[user]++
				coins--
			case 'i', 'I':
				distribution[user] += 2
				coins -= 2
			case 'o', 'O':
				distribution[user] += 3 //分金幣
				coins -= 3              //分出金幣之后需要從總金幣數量扣除
			case 'u', 'U':
				distribution[user] += 4
				coins -= 4

			}
		}

	}
	fmt.Println(distribution) //打印每人分到的金幣
	fmt.Println("剩余", coins)  //剩余的金幣
}
func main() {
	dispatchCoins()
}

  

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

參考


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM