GO 函數基礎知識

一、函數
1、函數的聲明
    func funcName(arg1 type,arg2 type,...)(output1 type, output2 type,...){
         return output1,output1,...
    }
2、函數的參數
     func funcName(arg1,arg2 type)(output1,opuput2 type){
          return output1,output1,...
     }
3、函數的返回值
    3.1 單返回值
         max := Max(a,b)
    3.2 多返回值
        sum,mutiplied := sumandProduct(a,b)
4、函數作爲參數
        max := Max(sumandProduct(a,b))
        函數在 GO 語言裏是第一類對象(一等公民)
5、函數做爲類型
       type name  func(arg1 type,arg2 type,...)(output1 type,output2 type,...)
       可以把函數當成值傳遞
       func function(f name)
6、可變參數
       func funcName(arg type,arg ...int){
           // TO DO
        }
7、匿名函數與閉包
     7.1 匿名函數
        func(arg1 type,arg2 type)(output1 type,...){
           // TO DO
        }
       不需要定義函數名,不能獨立存在,可以賦值到某個變量中(保存函數地址)
       調用
        func(arg1 type,arg2 type,...)(output1 type,...){
           // TO DO
        }(value1,value2,...)
     7.2 閉包 
         一個閉包集成了函數聲明時的作用域,這種狀態(作用域內的環境)會共享到閉包環境中,因此這些變量可以在閉包中被操作,直到被銷燬。
8、遞歸函數
     關鍵: 設置退出條件,否則遞歸將陷入無限循環。
9、參數傳遞機制
     9.1 按值傳遞
         操作副本,實參不變
     9.2 按引用傳遞
         操作實參,實參變化
     9.3 引用傳遞的好處
         節省內存、函數可修改操作對象的狀態
10、defer(延遲語句)
    10.1、用途
            回收資源、清理收尾等工作
    10.2、執行順序
           defer 是一個棧,遵循先入後出。return 之前逆序執行 defer 語句
11、error
    11.1、error 接口
          tyoe error interface{
              Error() string
          }
    11.2、自定義 error
          自定義結構體,實現 error 接口中的 Error() 方法
          type PathError struct{
              Op string
              Path string
              Err error
          }
          func(receiver *PathError) Error() string{
              return receiver.Op + " " + receiver.Path + " " + receiver.Err.Error()
          }
13、panic (恐慌)
      13.1 產生
             直接調用、運行時產生
      13.2 產生的結果
             可以中斷原有的控制流程,但是所在函數中 defer 函數會正常執行      
      13.2 panic 和 error 的區別使用
           導致關鍵流程出現不可修復性錯誤的情況使用 panic,其他情況使用 error
      13.4 panic 是最後手段,代碼中應該很少或者沒有 panic 語句     
14、recover
      14.1 作用
            讓程序 從 panic 恢復過來
      14.2 如何使用
           defer func(){
                recover()
           }()
        14.3 如何獲取 recover()的返回值
          

一、函數
在 GO 語言中,方法和函數有明顯的區分。函數是沒有接收者,而方法是有接收者。函數是 GO 語言中的第一對象。

1、聲明

func funcName(arg1 type,arg2 type,...)(output1 type,output2 type,...)

1.1 關鍵字 func 是用來聲明函數的
1.2 funcName 是指函數的名字,如果函數名字以大寫字母開頭,則該函數是公開的,可以被其他包調用。反之,小寫字母開頭,作用域只屬於所聲明的包
1.3 函數不支持嵌套(函數不能在其他函數體聲明)、重載、默認參數
1.4 函數作爲 GO 中第一對象,函數同樣可以通過聲明的方式作爲一個函數類型被使用,也可以賦值給變量

//作爲函數類型
type addNum func(a int, b int)(sum int)
//函數類型賦值給變量
add := addNum

1.5 函數之間是可以相互比較的,如果引用相同函數或者返回值都是 nil,認爲他們是相同函數。

2、函數的參數
函數可以有一個或多個參數,每個參數後面都有類型,通過","符號分隔。
如果參數列表中若干個相鄰參數的類型相同,可以省略前面變量的類型聲明
如果返回值列表中若干個相鄰參數類型相同,也可以用同樣的方式合併

//合併參數類型
func add(a,b int)(sum int,err error){
     //TO DO
}
//合併返回值類型
func sumandproduct(a,b int)(add,mutiplied int){
    //TO DO
}

3、函數的返回值
返回值就是函數執行後返回的內容,函數可以返回多個返回值。如果有返回值,那麼必須在函數的最後添加 return語句。在函數塊裏,return之後的語句都不會執行。對於返回值,可以不聲明變量,但是要指定變量類型。

3.1 單返回值

func max(a,b int) (max int)  {

	if a > b {
		max = a
	}
	max = b
	return max
}

3.2 多返回值

func sumandProduct(a,b int)(sum,mutiplied int)  {
	sum = a + b
	mutiplied = a * b
	return sum,mutiplied
}

注意: 多返回值也就意味着返回值有時候並不是全都需要的,我們可以用 “_” 忽略函數的返回值。

4、函數作爲參數
函數是第一類對象,可作爲參數傳遞。只要被調用函數的返回值個數、返回值類型和返回值順序與調用函數所需參數是一致的,就可以把這個被調用的函數當成其他函數的參數。

//f1 需要 3 個參數,f2 返回 3 個參數
func f1(a,b,c int)
func f2(a,b int(intintint)
func f1(f2(a,b))

例如,上面定義的函數,max(a,b int)需要兩個參數,都是int類型,剛好函數sumandProduct(a,b int)(sum,mutiplied int) 返回兩個參數,而且都是int類型,先不管順序一說,而套入上面的公式,就可以得到

func main() {
	maxValue := max(sumandProduct(3,5))
	fmt.Println(maxValue)
}

5、函數做爲類型
函數在 GO 中也算是一種變量,既然是變量,也可以用type(類型別名)將其定義類型。函數作爲類型最大的好處在於可以把這個類型的函數當成值傳遞

需求: 給定一組整型切片,返回都是偶數的新切片

func isOdd(value int) (result bool){
	return value % 2 == 0
}

//定義 isOdd 的類型別名,另其成爲值進行傳遞
type boolFunc func(value int) (result bool)

func filter(slice []int,f boolFunc) []int {
	var result []int
	for _,value := range slice{
		if f(value) { //函數調用
			result = append(result,value)
		}
	}
	return result
}
//在main方法中
func main() {

	newSlice := filter([]int{1,4,5,67,14,8},isOdd)
	fmt.Println(newSlice)
}

6、可變參數
在 GO 語言中,函數的 最後一個(變參函數可以只是函數的其中一個參數)參數如果 是...type的形式,那這個函數就是一個變參函數,可以處理變長的參數,而這個長度可以爲 0 。在變參函數中,無論變參多少個,他們的類型是一樣的。在函數體中,可以通過for index,value := range arg 的形式迭代訪問。

func searchMaxNumber(arg ...int) (max int) {
	max = arg[0]
	for _,value := range arg{
		if value > max{
			max = value
		}
	}
   return max
}

注意: 如果函數參數多個參數,其中一個是可變參數,可變參數必須放在參數列表中最後一個。

7、匿名函數與閉包

7.1 匿名函數
不需要定義函數名,不能獨立存在,可以賦值到某個變量中(保存函數地址),直接調用匿名函數後面要緊跟函數運行參數。

func main() {
    //變量 funcimplement 保存匿名函數的地址,並調用
 	funcimplement := func(a,b int)(sum int){
		return a + b
	}
	fmt.Println(funcimplement(5,8))
}

或者

func main(){

func block()  {
	func(a,b int)(mutiplied int){
		return a *b
	}(10,5)
}

注意: 參數列表中的第一對括號必須緊挨着關鍵字func,因爲匿名函數沒有名字。花括號{}涵蓋着函數體,最後一對括號表示對該匿名函數的調用。

7.2 閉包
閉包允許定義在其他環境下的變量,使得某個函數捕捉到一些外部狀態(局部變量)。 閉包函數保存其中外部狀態,不管外部函數是否退出,他都能夠繼續操作外部函數中的狀態(局部變量)
跟 Java 的匿名內部類一個意思

//定義一個函數,該函數的返回值是一個閉包。閉包訪問外部函數的變量
func block() func()int  {
	i := 0
	return func() int {
		i++
		return i
	}
}

func callBlock()  {

	//返回閉包,並保存閉包中訪問外部變量的狀態
	nextNumber := block()
	fmt.Println(nextNumber())  //1
	fmt.Println(nextNumber())  //2
	fmt.Println(nextNumber()) //3
}

8、遞歸函數
遞歸,就是在運行的過程中調用自己。使用遞歸時,開發者需要注意設置退出條件,否則就會陷入無限循環。

//斐波那契數列
func fibonacci(n int) int {
	if n < 2 {
		return n
	}
	return fibonacci(n - 2) + fibonacci(n - 1)
}
//main函數
func main() {

	for index := 0 ;index < 10 ;index++ {
		fmt.Printf("%d \t",fibonacci(index))
	}
}

9、參數傳遞機制

9.1 按值傳遞
GO 語言默認傳遞的是參數副本,函數在接到副本後,使用變量的過程中可能對副本進行更改,但不會影響原來的變量。換句話說,調用函數修改原來參數的值,不會影響原來實參的值,因爲數值變化只作用於副本上。

9.2 按引用傳遞
如果要讓函數直接修改參數的值,而不是對參數副本進行修改,就需要將參數的地址傳遞給函數,這個就是按引用傳遞,此時傳遞給函數的是一個指針。如果把指針傳遞給函數,指針的值就會被賦值,但指針的值指向的地址上的那個值不會被複制。這樣一來,修改指針的值,實際上意味着這個值所指向的地址上的值被修改了。

9.3 引用傳遞的好處
1、傳指針使得多個函數能夠操作同一個對象
2、傳指針比較輕量級(8 B),畢竟支持傳內存地址
3、傳遞指針給函數不但可以節省內存,而且賦予了函數直接修改外部變量變量的能力。

10、defer(延遲語句)

GO 語言中讓人頗爲稱道的一個設計就是 延遲語句,開發者可以在函數中添加多個defer語句。當函數執行到最後時(return 語句執行之前),這些defer會按照逆序執行,最後該函數才退出。

10.1、用途
在進行一些 IO 操作的時候,如果遇到錯誤,需要提前返回,返回之前需要關閉響應的資源,否則容易造成資源泄露。

func ReadWrite() bool{
     file.Open("file")
     if aFailure{       //發生b錯誤,提前返回,返回之前關閉資源
         file.Close()
         return false
     }else if bFailure{  //發生b錯誤,提前返回,返回之前關閉資源
          file.Close()
          return false
     }
     //正常執行返回
    file.Close()
    return ture
}

從上面我們可以看到,file.Close()重複了 3 次。我們可以用 defer 優雅解決問題。

func ReadWrite() bool{
     file.Open("file")
     defer file.Close()
     if aFailure{       //發生b錯誤,提前返回,返回之前關閉資源
         return false
     }else if bFailure{  //發生b錯誤,提前返回,返回之前關閉資源
          return false
     }
    return ture
}

10.2、執行順序
defer、return、返回值 三者的執行邏輯: defer 最先執行一些收尾工作;然後 return 執行,return 負責將結果寫入返回值中;最後函數攜帶當前返回值退出。這裏要特別注意: 函數的返回值是否被命名。

//無名返回值
func deferFunction1() int {

	var  i int
	//defer 一個匿名函數調用
	defer func(){
		i++
		fmt.Printf("defer2 := %d\n",i)
	}()
	//defer 另一個匿名函數調用
	defer func() {
		i++
		fmt.Printf("defer1 := %d\n",i)
	}()

	return i
}
//有名字的返回值
func deferFunction2() (i int ) {

	//defer 一個匿名函數調用
	defer func(){
		i++
		fmt.Printf("defer4 := %d\n",i)
	}()
	//defer 另一個匿名函數調用
	defer func() {
		i++
		fmt.Printf("defer3 := %d\n",i)
	}()

	return i
}
//main方法
func main() {

	fmt.Println(deferFunction1())
	fmt.Println(deferFunction2())
}

輸出結果:

defer1 := 1
defer2 := 2
0
defer3 := 1
defer4 := 2
2

11、error

11.1 error 接口
GO 語言通過 error 接口實現錯誤處理的標準模式。對於大部分語言而言,返回錯誤基本上都可以定義爲以下模式,即將 error 作爲多返回值中的最後一個返回值。當然,這個並非強制要求。

func name(arg1 type,...)(ouputvalue type, err error){
   // TO DO
}
//error 接口
tyoe error interface{
     Error() string
}

GO 語言中的接口十分靈活,不需要從 error 接口繼承或者像 Java 一樣使用 implements 來明確指定類型和接口之前的關係。

11.2 自定義錯誤
日常開發中,我們常常會根據業務制定一些列違背業務的錯誤。那就涉及到如何描述這個錯誤。下面我們舉例 GO 系統標準庫的實際代碼如果使用自定義的 error 類型。

type PathError struct {
	Op string
	Path string
	Err error
}

爲了讓編譯器知道 PathError 可以當成一個 error 來傳遞,我們還需要關鍵的一步,就是實現 error 接口。

func (p *PathError) Error() string {
	return p.Op + " " +  p.Path + " 具體異常信息=>" + p.Err.Error()
}

我舉個例子,類型轉換異常,字符串轉數字失敗的情況。

func errorInterface()  {

	//這裏會報一個錯誤(類似 Java 的類型轉化異常的東西)
	bool, error := strconv.ParseBool("haha")
	if error != nil{

		customError := new(PathError)
		customError.Op = "類型轉換異常"
		customError.Path = "錯誤參數:haha"
		customError.Err = error
		fmt.Println(customError)

	   //fmt.Println(reflect.TypeOf(error))
	   //strconv.ParseBool: parsing "haha": invalid syntax
		//fmt.Println(error)
	}else {
		fmt.Println(bool)
	}
}

輸出結果:

類型轉換異常 錯誤參數:haha 具體異常信息=>strconv.ParseBool: parsing "haha": invalid syntax

從上我們可以看到,字符串轉化成數字失敗了,有error != nil,但是我又想將其 error描述成白話文,也就是一眼就能看到關鍵的錯誤信息。我就對實際錯誤 strconv.ParseBool: parsing "haha": invalid syntax進行了包裝,包裝成PathError 對象。但是,實際開發中,我們並不滿足於只打印一句錯誤信息,我們還需要掌握更多錯誤信息,方便調試。發生錯誤時,確定是 PathError類型錯誤,獲取更多屬性,方便定位和處理問題。這個就需要類型轉化的知識。這裏簡單介紹一下。

fi,err :=  os.start("a.text")
if err != nil{
   if e,ok := err.(*os.PthError);ok && e.Err != nil{
      // TO DO
      //獲取 PthError 類型變量 e 中的其他信息並處理
   }
}

13、panic (恐慌)
panic()是一個內建函數,可以中斷原有的控制流程,當 一個 panic 產生,表示程序運行遇到問題,不知道到該怎麼辦。當 函數 func1 調用 panic 時,函數會被中斷執行,但是 func1 中的 defer 函數會正常執行。然後 func1 返回調用他的地方。錯誤信息將被報告,包括在調用 panic() 函數時傳入的參數,這個過程稱爲錯誤處理流程。

func panic(v interface{})

13.1 產生

  直接調用、運行時產生。 例如: 數組越界。
  panic()函數接口任意類型的數據。interface{} 表示任意類型。類似 OC 中的 id 類型

13.2 產生的結果
可以中斷原有的控制流程 ,但是 函數所在 defer 函數會正常執行。

func deferPanic()  {

	// 延遲 一個匿名函數的調用
	defer func() {
		panic("defer panic")
	}()
	panic("test panic")
}
func main() {
	deferPanic()
}

輸出結果:

panic: test panic
        panic: defer panic

13.2 panic 和 error 的區別使用
導致關鍵流程出現不可修復性錯誤的情況使用 panic,
其他情況使用 error

13.4 panic 是最後手段,代碼中應該很少或者沒有 panic 語句

14、recover

14.1 作用
recover() 也是一個內建函數。可以讓處於 panic狀態的程序恢復過來。如果當前程序處於 panic 狀態,調用 reccover() 可以捕獲到 panic 的輸入值,並且恢復正常的執行。

14.2 如何使用
由於 recover()用於終止錯誤處理流程,所以一般情況下,recover()僅在 defer語句的函數內 直接調用 纔有效,否則總是返回 nil。

func test()  {

	defer func() {
		//有效,在 defer 語句中的匿名函數中調用
		fmt.Println(recover())
	}()

	defer func() {
		//無效,間接調用 recover() ,返回值 nil
		func(){
			recover()
		}()
	}()
	//無效 recover()相當於直接調用然後被外部函數打印,返回 nil
	defer fmt.Println(recover())

	//無效,相當於 直接調用 recover(),返回 nil
	defer recover()

	panic("發生錯誤")
}

14.3 如何獲取 recover()的返回值

func recoverFunction()  {
	
	defer func() {
		if r := recover(); r != nil {
			log.Printf("捕獲到的異常:%v\n",r)
		}
	}()
	defer func() {
		panic("第二個錯誤")
	}()
	panic("第一個錯誤")
}

輸出結果:

2020/06/26 23:18:22 捕獲到的異常:第二個錯誤

“第一個錯誤” 早就發生了,但是 recover()函數中看不到 "第一個錯誤"的捕獲時間,這個因爲 recover()只會捕獲最後一個錯誤,而且捕獲時機實在函數最後面,不影響"第一個錯誤"之後的defer正常執行。最後,程序正常退出。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章