由於自己是搞python開發的,所以在學習go時,當看到函數和方法時,頓時還是挺蒙的,因為在python中並沒有明顯的區別,但是在go中卻是兩個完全不同的東西。在官方的解釋中,方法是包含了接收者的函數。
定義
函數的格式是固定的
Func + 函數名 + 參數 + 返回值(可選) + 函數體
Func main( a, b int) (int) {
}
而方法會在方法在func關鍵字后是接收者而不是函數名,接收者可以是自己定義的一個類型,這個類型可以是struct,interface,甚至我們可以重定義基本數據類型。不過需要注意的是接收者是指針和非指針的區別,我們可以看到當接收者為指針式,我們可以通過方法改變該接收者的屬性,但是非指針類型缺做不到。
func (p myint) mysquare() int {
p = p * p
fmt.Println("mysquare p = ", p)
return 0
}
函數
函數的值(閉包)
在Go中,函數被看作第一類值(first-class values):函數像其他值一樣,擁有類型,可以被賦值給其他變量,傳遞給函數,從函數返回。函數類型的零值是nil。調用值為nil的函數值會引起panic錯誤:
var f func(int) intf(3) // 此處f的值為nil, 會引起panic錯誤
函數值不僅僅是一串代碼,還記錄了狀態。Go使用閉包(closures)技術實現函數值,Go程序員也把函數值叫做閉包。我們看個閉包的例子
func f1(limit int) (func(v int) bool) {
//編譯器發現limit逃逸了,自動在堆上分配
return func (limit int) bool { return v>limit}
}
func main() {
closure := f1(5)
fmt.Printf("%v\n", closure(1)) //false
fmt.Printf("%v\n", closure(5)) //false
fmt.Printf("%v\n", closure(10)) //true
}
在這個例子中,f1函數傳入limit參數,返回一個閉包,閉包接受一個參數v,判斷v是否大於之前設置進去的limit。
2 可變參數列表
在go中函數提供可變參數,對那些封裝不確定參數個數是一個不錯的選擇。聲明如下
func 函數名(變量名...類型) 返回值
package main
import (
"fmt"
)
func f1(name string, vals... int) (sum int) {
for _, v := range vals {
sum += v
}
sum += len(name)
return
}
func main() {
fmt.Printf("%d\n", f1("abc", 1,2,3,4 )) //13
}
在函數中提供延遲執行即 defer
包含defer語句的函數執行完畢后(例如return、panic),釋放堆棧前會調用被聲明defer的語句,常用於釋放資源、記錄函數執行耗時等,有一下幾個特點:
當defer被聲明時,其參數就會被實時解析
執行順序和聲明順序相反
defer可以讀取有名返回值
運用最典型的場景及關閉資源,如操作文件,數據庫操作等。如下例子
func do() error {
f, err := os.Open("book.txt")
if err != nil {
return err
}
defer func(f io.Closer) {
if err := f.Close(); err != nil {
// log etc
}
}(f)
// ..code...
f, err = os.Open("another-book.txt")
if err != nil {
return err
}
defer func(f io.Closer) {
if err := f.Close(); err != nil {
// log etc
}
}(f)
return nil
}
異常panic
在開始閉包中提到過返回panic,那什么是panic。Go有別於那些將函數運行失敗看作是異常的語言。雖然Go有各種異常機制,但這些機制僅僅用於嚴重的錯誤,而不是那些在健壯程序中應該被避免的程序錯誤。runtime在一些情況下會拋出異常,例如除0,我們也能使用panic關鍵字自己拋出異常。
出現異常,默認程序退出並打印堆棧。如下函數
package main
func f6() {
func () {
func () int {
x := 0
y := 5/x
return y
}()
}()
}
func main() {
f6()
}
如果不想程序退出的話,也有辦法,就是使用recover捕捉異常,然后返回error。在沒發生panic的情況下,調用recover會返回nil,發生了panic,那么就是panic的值。看個例子:
package main
import (
"fmt"
)
type shouldRecover struct{}
type emptyStruct struct{}
func f6() (err error) {
defer func () {
switch p := recover(); p {
case nil: //donoting
case shouldRecover{}:
err = fmt.Errorf("occur panic but had recovered")
default:
panic(p)
}
} ()
func () {
func () int {
panic(shouldRecover{})
//panic(emptyStruct{})
x := 0
y := 5/x
return y
}()
}()
return
}
func main() {
err := f6()
if err != nil {
fmt.Printf("fail %v\n", err)
} else {
fmt.Printf("success\n")
}
}
方法
package main
import (
"fmt"
)
type Employee struct {
name string
salary int
currency string
}
/*
displaySalary() method has Employee as the receiver type
*/
func (e Employee) displaySalary() {
fmt.Printf("Salary of %s is %s%d", e.name, e.currency, e.salary)
}
func main() {
emp1 := Employee{
name: "Sam Adolf",
salary: 5000,
currency: "$",
}
emp1.displaySalary() //Calling displaySalary() method of Employee type
}
也許有人會問,方法和函數差不多,為什么還要多此一舉使用方法呢?
- Golang 不是一個純粹的面向對象的編程語言,它不支持類。因此通過在一個類型上建立方法來實現與 class 相似的行為。
- 同名方法可以定義在不同的類型上,但是 Golang 不允許同名函數。假設有兩個結構體 Square 和 Circle。在 Square 和 Circle 上定義同名的方法是合法的。
如下一個函數就很明了了
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())
}
值接收者和指針接收者
兩者區別在於,以指針作為接收者時,方法內部進行的修改對於調用者是可見的,但是以值作為接收者卻不是。
package main
import (
"fmt"
)
type Employee struct {
name string
age int
}
/*
Method with value receiver
*/
func (e Employee) changeName(newName string) {
e.name = newName
}
/*
Method with pointer receiver
*/
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)
e.changeName("Michael Andrew")
fmt.Printf("\nEmployee name after change: %s", e.name)
fmt.Printf("\n\nEmployee age before change: %d", e.age)
(&e).changeAge(51)
fmt.Printf("\nEmployee age after change: %d", e.age)
}
上面的程序中, changeName 方法有一個值接收者 (e Employee),而 changeAge 方法有一個指針接收者 (e *Employee)。在 changeName 中改變 Employee 的 name 的值對調用者而言是不可見的,因此程序在調用 e.changeName("Michael Andrew") 方法之前和之后,打印的 name 是一樣的。而 changeAge 的接受者是一個指針 (e *Employee),因此通過調用方法 (&e).changeAge(51) 來修改 age 對於調用者是可見的。
使用 (&e).changeAge(51) 來調用 changeAge 方法不是必須的,Golang 允許我們省略 & 符號,因此可以寫為 e.changeAge(51)。Golang 將 e.changeAge(51) 解析為 (&e).changeAge(51)。
非結構體類型的方法
現在我們定義的都是結構體類型的方法,同樣可以定義非結構體類型的方法,不過需要注意一點。為了定義某個類型的方法,接收者類型的定義與方法的定義必須在同一個包中。
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)
}
在函數和方法中都會接收值參數和指針參數,那么兩者又有什么卻別?
方法的值接收者和函數的值參數
當一個函數有一個值參數時,它只接受一個值參數。
當一個方法有一個值接收者時,它可以接受值和指針接收者。
如下一個例子
package main
import (
"fmt"
)
type rectangle struct {
length int
width int
}
func area(r rectangle) {
fmt.Printf("Area Function result: %d\n", (r.length * r.width))
}
func (r rectangle) area() {
fmt.Printf("Area Method result: %d\n", (r.length * r.width))
}
func main() {
r := rectangle{
length: 10,
width: 5,
}
area(r)
r.area()
p := &r
/*
compilation error, cannot use p (type *rectangle) as type rectangle
in argument to area
*/
//area(p) //會報錯
p.area() //calling value receiver with a pointer
}
我們創建了一個指向 r 的指針 p。如果我們試圖將這個指針傳遞給只接受值的 area 函數那么編譯器將報錯。
p.area() 使用指針接收者 p 調用一個值接收者方法 area 。這是完全合法的。原因是對於 p.area(),由於 area 方法必須接受一個值接收者,所以 Golang 將其解析為 (*p).area()。
方法的指針接收者和函數的指針參數
具有指針參數的函數將僅接受指針,而具有指針接收者的方法將接受值和指針接收者
package main
import (
"fmt"
)
type rectangle struct {
length int
width int
}
func perimeter(r *rectangle) {
fmt.Println("perimeter function output:", 2*(r.length+r.width))
}
func (r *rectangle) perimeter() {
fmt.Println("perimeter method output:", 2*(r.length+r.width))
}
func main() {
r := rectangle{
length: 10,
width: 5,
}
p := &r //pointer to r
perimeter(p)
p.perimeter()
/*
cannot use r (type rectangle) as type *rectangle in argument to perimeter
*/
//perimeter(r)
r.perimeter() //calling pointer receiver with a value
}
試圖以一個值參數 r 調用 perimeter 函數,這是非法的。因為一個接受指針為參數的函數不能接受一個值作為參數。如果去掉注釋,則編譯報錯。
通過一個值接收者 r 調用一個指針接收者 perimeter 方法,這是合法的。r.perimeter() 將被 Golang 解析為 (&r).perimeter()。
