Golang 學習筆記六 函式和方法的區別
在接觸到go之前,我認為函式和方法只是同一個東西的兩個名字而已(在我熟悉的c/c++,python,java中沒有明顯的區別),但是在golang中者完全是兩個不同的東西。官方的解釋是,方法是包含了接收者的函式。
一、函式
1.定義
函式宣告包括函式名、形式引數列表、返回值列表( 可省略) 以及函式體。
func name(parameter-list) (result-list) { body }
比如
func hypot(x, y float64) float64 { return math.Sqrt(x*x + y*y) } fmt.Println(hypot(3,4)) // "5"
正如hypot一樣,如果一組形參或返回值有相同的型別,我們不必為每個形參都寫出引數型別。下面2個宣告是等價的:
func f(i, j, k int, s, t string) { /* ... */ } func f(i int, j int, k int, s string, t string) { /* ... */ }
每一次函式呼叫都必須按照宣告順序為所有引數提供實參( 引數值)。在函式呼叫時,Go語言沒有預設引數值 ,也沒有任何方法可以通過引數名指定形參,因此形參和返回值的變數名對於函式呼叫者而言沒有意義。
2.實參通過值的方式傳遞,因此函式的形參是實參的拷貝。對形參進行修改不會影響實參。但是,如果實參包括引用型別,如指標,slice(切片)、map、function、channel等型別,實參可能會由於函式的簡介引用被修改。
3.多返回值舉例
func findLinks(url string) ([]string, error) { resp, err := http.Get(url) if err != nil { return nil, err } if resp.StatusCode != http.StatusOK { resp.Body.Close() return nil, fmt.Errorf( "getting %s: %s", url, resp.Status) } doc, err := html.Parse(resp.Body) resp.Body.Close() if err != nil { return nil, fmt.Errorf( "parsing %s as HTML: %v", url, err) } return visit(nil, doc), nil }
呼叫多返回值函式時,返回給呼叫者的是一組值,呼叫者必須顯式的將這些值分配給變數:links, err := findLinks(url)
。 如果某個值不被使用,可以將其分配給blank identifier:links, _ := findLinks(url) // errors ignored
4.匿名函式
// squares返回一個匿名函式。 // 該匿名函式每次被呼叫時都會返回下一個數的平方。 func squares() func() int { var x int return func() int { x++ return x * x } } func main() { f := squares() fmt.Println(f()) // "1" fmt.Println(f()) // "4" fmt.Println(f()) // "9" fmt.Println(f()) // "16" }
squares的例子證明,函式值不僅僅是一串程式碼,還記錄了狀態。在squares中定義的匿名內部函式可以訪問和更新squares中的區域性變數,這意味著匿名函式和squares中,存在變數引用。這就是函式值屬於引用型別和函式值不可比較的原因。Go使用閉包( closures) 技術實現函式值,Go程式設計師也把函式值叫做閉包。
通過這個例子,我們看到變數的生命週期不由它的作用域決定:squares返回後,變數x仍然隱式的存在於f中。
5.可變引數
在宣告可變引數函式時,需要在引數列表的最後一個引數型別之前加上省略符號“...”,這表示該函式會接收任意數量的該型別引數。
func sum(vals...int) int { total := 0 for _, val := range vals { total += val } return total }
sum函式返回任意個int型引數的和。在函式體中,vals被看作是型別為[] int的切片。sum可以接收任意數量的int型引數。
fmt.Println(sum()) // "0" fmt.Println(sum(3)) // "3" fmt.Println(sum(1, 2, 3, 4)) // "10"
在上面的程式碼中,呼叫者隱式的建立一個數組,並將原始引數複製到陣列中,再把陣列的一個切片作為引數傳給被調函式。如果原始引數已經是切片型別,我們該如何傳遞給sum?只需在最後一個引數後加上省略符。下面的程式碼功能與上個例子中最後一條語句相同。
values := []int{1, 2, 3, 4} fmt.Println(sum(values...)) // "10"
二、方法
1.定義
在函式宣告時,在其名字之前放上一個變數,即是一個方法。這個附加的引數會將該函式附加到這種型別上,即相當於為這種型別定義了一個獨佔的方法。
package geometry import "math" type Point struct{ X, Y float64 } // traditional function func Distance(p, q Point) float64 { return math.Hypot(q.X-p.X, q.Y-p.Y) } // same thing, but as a method of the Point type func (p Point) Distance(q Point) float64 { return math.Hypot(q.X-p.X, q.Y-p.Y) }
上面的程式碼裡,那個在關鍵字func和函式名之間附加的引數p,叫做方法的接收器(receiver),早期的面嚮物件語言留下的遺產將呼叫一個方法稱為“向一個物件傳送訊息”。在Go語言中,我們並不會像其它語言那樣用this或者self作為接收器;我們可以任意的選擇接收器的名字。
p := Point{1, 2} q := Point{4, 6} fmt.Println(Distance(p, q)) // "5", function call fmt.Println(p.Distance(q)) // "5", method call
可以看到,上面的兩個函式呼叫都是Distance,但是卻沒有發生衝突。第一個Distance的呼叫實際上用的是包級別的函式geometry.Distance,而第二個則是使用剛剛宣告的Point,呼叫的是Point類下宣告的Point.Distance方法。
2.接收者有兩種型別:值接收者和指標接收者
值接收者,在呼叫時,會使用這個值的一個副本來執行。
type user struct{ name string email string } func (u user) notify(){ fmt.Printf("Sending user email to %s <%s>\n", u.name, u.email ); } // bill := user("Bill","[email protected]"); bill.notify() // lisa := &user("Lisa","[email protected]"); lisa.notify()
這裡lisa使用了指標變數來呼叫notify方法,可以認為go語言執行了如下程式碼
(*lisa).notify()
go編譯器為了支援這種方法呼叫,將指標解引用為值,這樣就符合了notify方法的值接收者要求。再強調一次,notify操作的是一個副本,只不過這次操作的是從lisa指標指向的值的副本。
3.指標接收者
func (u *user) changeEmail(email string){ u.email = email } lisa := &user{"Lisa","[email protected]"} lisa.changeEmail("[email protected]");
當呼叫使用指標接收者宣告的方法時,這個方法會共享呼叫方法時接收者所指向的值。也就是說,值接收者使用值的副本來呼叫方法,而指標接收者使用實際值來呼叫方法。
也可以使用一個值來呼叫使用指標接收者宣告的方法
bill := user{"Bill","[email protected]"} bill.changeEmail("[email protected]");
實際上,go編譯器為了支援這種方法,在背後這樣做
(&bill).changeEmail("[email protected]");
go語言既允許使用值,也允許使用指標來呼叫方法,不必嚴格符合接收者的型別。
4.總結
- 不管你的method的receiver是指標型別還是非指標型別,都是可以通過指標/非指標型別進行呼叫的,編譯器會幫你做型別轉換。
- 在宣告一個method的receiver該是指標還是非指標型別時,你需要考慮兩方面的內部,第一方面是這個物件本身是不是特別大,如果宣告為非指標變數時,呼叫會產生一次拷貝;第二方面是如果你用指標型別作為receiver,那麼你一定要注意,這種指標型別指向的始終是一塊記憶體地址,就算你對其進行了拷貝。熟悉C或者C艹的人這裡應該很快能明白。