43

Golang 学习笔记六 函数和方法的区别

 5 years ago
source link: https://studygolang.com/articles/17950?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

在接触到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艹的人这里应该很快能明白。

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK