golang 函數和方法


由於自己是搞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()。

 

 


免責聲明!

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



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