1. 程式人生 > 實用技巧 >Go語言——函式和方法

Go語言——函式和方法

函式

函式宣告

  go語言函式支援多返回值

func name(parameter-list)(result-list){

  body

}

  當函式存在返回列表的時候,必須顯示以return結束。(除了break,拋異常等操作)

func sub(x,y int) (z int){ z = x-y;return} //如果形參型別相同,可以寫到一起;返回值也可以命名,這時候,每個命名的返回值會宣告一個區域性變數,並初始化零值,最後跟一個return即可

func first(x int,_ int) int{return x} //空白標識強調這個形參在函式中未使用

func zero(int,int) int{return 0}

  形參和返回值名字不會影響函式簽名。

  Go語言沒有預設引數值概念也不能指定實參名(和python不一樣)。go語言是值傳遞,指標、slice、map、函式、通道是引用型別,使用時可能會間接改變實慘值。

  如果函式的宣告沒有函式體,就說明這個函式使用了除了go以外的語言實現。

遞迴

  許多程式語言使用固定長度的函式呼叫棧,大小在64KB到2MB之間。遞迴的深度會受限於固定長度的棧大小,所以當進行深度遞迴呼叫時必須提防棧溢位。相比於固定長度的棧,Go語言的實現使用了可變長度的棧,棧的大小會隨著使用而增長,可達到1GB左右上限。

多返回值

  題外話:Go語言的垃圾回收機制將會回收未使用的記憶體,但不能指望它會釋放未使用的作業系統資源,比如開啟的檔案以及網路連線,必須顯式的關閉它們。

  呼叫者使用多返回值的函式時候,必須有變數承接返回值,如果不想使用,用'_'承接。

func f(a string)([]string,error){
    /*...*/
    return f(a); 
} //返回一個多值結果可以是呼叫另一個多值返回的函式


//一個多值呼叫可以作為單獨的實參傳遞給擁有多個形參的函式中
log.Println(f(a))

//等價於
strings, err:=f(a)
log.Println(strings,err)

錯誤

  即使在高質量程式碼中,也不能保證一定能夠成功返回,習慣上將錯誤值作為最後一個結果返回。如果錯誤只有一種情況,結果通常設定為布林型別

  如果錯誤原因很多,那麼錯誤型別往往是error,error是內建的介面型別。目前我們瞭解到,錯誤可能是空值(成功)和非空值(失敗),通過呼叫fmt.Println(err)或fmt.Printf("%v",err)輸出錯誤資訊。與其他語言不一樣,go語言通過使用普通的值而非異常來報告錯誤。

錯誤處理策略

  首先最常用的是將錯誤傳遞下去:return nil,fmt.Errorf("parsing %s as HTML: %v",url,err)。錯誤返回資訊要可讀、有意義、格式一致。

  第二種情況是對於不固定或者不可預測的錯誤,在短暫的間隔後對操作進行重試是合乎情理的,超出一定的重試次數和限定時間後再報錯退出。

  如果還不能順利執行,呼叫者能夠輸出錯誤然後優雅的停止程式。

if err:=WaitForServer(url);err!=nil{
  fmt.Fprintf(os.Stderr,"Site is down:%v\n",err)
  os.Exit(1)

  //更加方便的呼叫
  //log.Fatalf("Site is down:%v\n",err)
}

  第四,在一些錯誤情況下,只記錄錯誤資訊然後程式繼續執行。

  第五,在某些罕見的情況下我們可以直接安全地忽略掉整個日誌。

  go語言的錯誤處理有特定的規律,一般都是開頭一連串的檢查用來返回錯誤,檢測到失敗往往都是在成功之前。

檔案結束標識

  io包保證任何由檔案結束引起的讀取錯誤,始終都將會得到一個與眾不同的錯誤——io.EOF。

package io
import "errors"

var EOF = errors.New("EOF")

//示例
in:=bufio.NewReader(os.Stdin)
for{
    r,_,err:=in.ReadRune()
    if err==io.EOF{
        break
    }    
    if err != nil{
        return fmt.Errorf("read failed:%v",err)
    }
}

函式變數

  類似於C語言中函式指標的概念。函式變數的型別就是函式的簽名,它們可以賦給變數或者傳遞或者從其他函式中返回。用法基本和C語言中的函式指標一樣。

func square(n int) int{return n*n}
func product(man int) int{return m*n}

f:=square
f(3)

f=product //錯誤,型別不一致

var f func(int) int //定義一個空值函式變數
f(3) //宕機:呼叫空函式

//函式變數只能和nil比較

匿名函式

  匿名函式的作用類似於java裡面的匿名函式,通常一些短小不常用的函式嵌入到呼叫者形參裡。匿名函式func關鍵字後面沒有函式名,他的值是一個函式變數。

func squares() func() int{
    var x int
    return func()int{
        x++
        return x*x
    } //匿名函式可以使用外層的變數
}

func main(){
f:=suares()
  fmt.Println(f()) //1
  fmt.Println(f()) //4
  fmt.Println(f()) //9
}

  函式變數類似於使用閉包方法實現的變數,Go程式設計師通常把函式變數稱為閉包(閉包就是能夠讀取其他函式內部變數的函式,所以閉包可以理解成“定義在一個函式內部的函式“)。我們看到變數的生命週期不是由它的作用域決定的,由於在定義squares( )函式時指定了返回的型別是一個匿名函式,並且該匿名函式返回的型別是整型。所以在squares( )函式中定義了一個匿名函式,並且將整個匿名函式返回,匿名函式返回的是整型。在main( )函式中定義了一個變數f,該變數的型別是匿名函式,f( )表示呼叫執行匿名函式。最終執行完成後發現,實現了數字的累加。雖然squares()已經返回了,但是返回的值:func()還在全域性變數中使用,三次呼叫 f(),因此返回值會儲存在堆上,即使棧釋放了記憶體資源,但func()儲存在堆中,資料不會釋放。

  因為匿名函式(閉包),有一個很重要的特點:

  它不關心這些捕獲了的變數和常量是否已經超出了作用域,所以只有閉包還在使用它,這些變數就還會存在

  如果匿名函式需要進行遞迴,必須先宣告一個變數然後將你ing函式賦值給這個變數。如果將兩個步驟合成一個生命,函式字面量將不能存在於函式變數的作用域中,這樣也就不能遞迴呼叫自己了。

visitALL:=func(items []string){
    //...
    visitAll[m[item]] //compile error: undefined:visitAll
    //...
}

警告:捕獲迭代變數

  這個是go語言詞法作用域規則的陷阱,即使是有經驗的程式設計師也會掉入這個陷阱。

var rmdirs []func()
for _,d:=range tempDirs(){
  dir:=d  //這一行是必須的
  os.MakeAll(dir,0755)
  rmdir = append(rmdir,func(){
    os.RemoveAll(dir)
  })
}

for _,rmdir:=range rmdirs{
  rmdir()
}

  為什麼需要在迴圈體內將迴圈變數賦給一個新的區域性變數dir,而不是直接使用?這個原因是迴圈變數的作用域的規則限制。在迴圈裡建立的所有函式變數共享相同的變數——一個可訪問的儲存位置,而不是一個固定的值。dir變數的值在不斷地迭代更新,因此當呼叫清理函式的時候,dir變數已經被每一次for迴圈更新多次,因此dir變數的實際值是最後一次迭代時的值。

//當需要儲存迭代變數的時候,我們通常宣告一個同名變數去飲用它
for_,dir:=range tempDirs(){
    dir:=dir
//...    
}

變長函式

  在引數列表最後的型別名稱之前使用省略號“...”表示宣告一個變長函式,呼叫這個函式的時候可以傳遞該型別任意數目的引數。

func sum(val ...int)int{
    //...
}

sum(1,2,3,4)

//實參是slice時候,在最後一個引數後面放一個省略號
values:=[]int{1,2,3,4}
sum(values...)

延遲函式呼叫

  語法上,一個defer語句就是在普通函式或方法的呼叫之前加上defer關鍵字。無論是正常情況還是異常情況,實際的呼叫推遲到包含defer語句的函式結束後才執行,defer語句經常用於成對的操作,比如開啟和關閉,加鎖和解鎖,即使是再複雜的控制流,資源在任何情況下都能夠正確釋放。正確使用defer語句的地方是在成功獲得資源之後

func ReadFile(filename string)([]byte,error){
  f,err:=os.Open(filename)
  if err!=nil{
    return nil,err  
  }
  defer f.Close()
  return ReadAll(f)
}

//解鎖一個互斥鎖
var mu sync.Mutex
var m=make(map[string]int)
func lookup(key string) int{
  mu.Lock()
  defer mu.Unlock()
  return m[key]
}

  延遲執行的函式在return語句之後執行,並且可以更新函式的結果變數。因為匿名函式可以捕獲其外層函式作用域內的變數,所以延遲執行的匿名函式可以觀察到函式的返回結果。

func double(x int)int{
    return x+ x
}

//增加defer
func double(x int)(result int){
    defer func(){fmt.Printf("dounble(%d) = %d\n",result)}() //圓括號不要忘記
    return x+x
}

宕機(panic函式)

  出現執行時錯誤時會發生宕機,正常程式執行會終止,goroutine中的所有延遲函式會執行,然後程式會異常退出並留下一條日誌訊息。當碰到“不可能發生”的狀況時,呼叫內建的宕機函式是最好的處理方式

//只有發生嚴重錯誤時才會使用宕機,否則會毫無意義,只會帶來多餘的檢查
func Reset(x *Buffer){
    if x==nil{
        panic("x is nil")
    }
    x.elements = nil
}

  當宕機發生時,所有延遲函式以倒序執行,從棧的最上面函式開始一直返回到main函式。

恢復(recover函式)

  退出程式通常是正確處理宕機的方式,但也有例外,在一定情況是可以進行恢復的,至少有時候可以在退出前理清當前混亂的情況。如果內建的recover函式在延遲函式的內部呼叫,而且這個包含defer語句的函式發生宕機,revover會終止當前的宕機狀態並且返回宕機的值。函式不會從之前宕機的地方繼續執行而是正常返回。如果recover在其他任何情況下執行則它沒有任何效果且返回nil。

func Parse(input string)(s *Syntax,err error){
    defer func(){
        if p:=recover();p!=nil{
            err = fmt.Error("internal error:%v",p)
        }
    }()
}

方法

  這裡方法的概念就是面向物件程式設計中的概念,go語言也支援面向物件程式設計,只不過沒有class概念,取而代之的是用struct來完成面向物件。

方法宣告

  我們知道,方法是屬於某一個類的,因此,在go語言中,只是在函式名字前面多一個引數,這個引數把這個方法繫結到引數對應的struct上。

type Point struct{x,y float64}

//普通函式
func Distance(p,q Point) float64{
  return math.Hypot(q.x-p.x,q.y=p.y)
}//包級別

//Point型別方法
func (p Point) Distance(q Point) float64{
  return math.Hypot(q.x-p.x,q.y-p,y)
}//這兩個函式不會衝突,Distance相當於在struct結構裡裡面,和x,y在同一個作用域

  附加引數p稱為方法接受者,go語言中,接受者不適用特殊名字(this,self),而是我們自己選擇。(最常用方法是取型別名稱首字母)。

  go和其他語言不一樣,它可以將方法繫結到任何型別上。可以很方便的為簡單型別定義附加行為。型別擁有的方法名必須是唯一的,但不同型別可以使用相同方法名。

指標接受者的方法

  習慣上遵循如果Point的任何一個方法使用指標接受者,那麼所有的Point方法都應該使用指標接受者。當實參接收者很大的時候,為了避免複製開銷,通常將這種接受者定義為指標接收者

func (p *Point) ScaleBy(factor float64){
    p.x*=factor
    p.y*=factor
}

//呼叫
r:=&Point{1,3}
r.ScaleBy(2)

p:=Point{1,2}
p.ScaleBy(2) //等價於(&)p.ScaleBy(2),編譯器會隱式轉換,只有變數才允許這麼做。

//不能夠對一個不能取地址的接受者引數呼叫*Point方法,因為無法獲取臨時變數地址
Point{1,2}.ScaleBy(2) //compile error

  如果實參接受者是*Point型別,呼叫Point.Distance方法是合法的,因為我們有辦法從地址中獲取Point值。

//這兩個呼叫效果一樣,編譯器也會隱式轉換
pptr.Distance(q)
(*pptr).Distance(q)

nil是一個合法的接收者

  就像一些函式允許nil指標作為實參,方法的接受者也一樣。

通過結構體內嵌組成型別

type ColoredPoint struct{
    Point
    Color color.RGBA
}

  內嵌相當於把Point裡面的東西都嵌入到ColoredPoint中,包括變數和方法,有點類似於繼承的意思。因此我們可以定義ColoredPoint型別,使用Point方法。

//如果形參不一致,需要顯式使用它
var p:=ColoredPoint{***}
var q:=ColoredPoint{***}

p.Distance(q.Point) //顯示使用

方法變數與表示式

  通常呼叫方法都是使用p.Distance()形式,但是把這兩個操作分開也是可以的。方法變數類似於函式變數,選擇子p.Distance可以賦予一個方法變數,即把方法繫結到接收者上,函式只需要提供實參而不需要提供接受者就能夠呼叫。

p:=Point{1,2}
q:=Point{3,4}

distanceFromP:=p.Distance //方法變數
distanceFromP(q) //不需要接收者就可以呼叫

  與方法變數相關的是方法表示式,把原來方法的接受者替換成函式的第一個形參,可以像呼叫平常函式一樣呼叫方法。

distance:=Point.Distance //方法表示式
distance(p,q)

  如果需要用一個值來代表多個方法,方法變數可以幫助你呼叫這個值所對應的方法來處理不同的接受者。

封裝

  封裝也被稱為資料隱藏,go語言只有一種方式控制命名可見性:首字母的大小寫。而在go語言中封裝的單元是包而不是型別,也就是說,結構體內的欄位對於同一個包都是可見的。在go語言的getter器命名時候通常將Get省略,比如Point型別中的X的getter和setter分別為X()和SetX()。