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

懒皮 · · 622 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

参考
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","bill@email.com");
bill.notify()

//
lisa := &user("Lisa","lisa@email.com");
lisa.notify()

这里lisa使用了指针变量来调用notify方法,可以认为go语言执行了如下代码

(*lisa).notify()

go编译器为了支持这种方法调用,将指针解引用为值,这样就符合了notify方法的值接收者要求。再强调一次,notify操作的是一个副本,只不过这次操作的是从lisa指针指向的值的副本。

3.指针接收者

func (u *user) changeEmail(email string){
    u.email = email
}
lisa := &user{"Lisa","lisa@email.com"}
lisa.changeEmail("lisa@newdomain.com");

当调用使用指针接收者声明的方法时,这个方法会共享调用方法时接收者所指向的值。也就是说,值接收者使用值的副本来调用方法,而指针接收者使用实际值来调用方法。

也可以使用一个值来调用使用指针接收者声明的方法

bill := user{"Bill","bill@email.com"}
bill.changeEmail("bill@newdomain.com");

实际上,go编译器为了支持这种方法,在背后这样做

(&bill).changeEmail("bill@newdomain.com");

go语言既允许使用值,也允许使用指针来调用方法,不必严格符合接收者的类型。

4.总结

  • 不管你的method的receiver是指针类型还是非指针类型,都是可以通过指针/非指针类型进行调用的,编译器会帮你做类型转换。
  • 在声明一个method的receiver该是指针还是非指针类型时,你需要考虑两方面的内部,第一方面是这个对象本身是不是特别大,如果声明为非指针变量时,调用会产生一次拷贝;第二方面是如果你用指针类型作为receiver,那么你一定要注意,这种指针类型指向的始终是一块内存地址,就算你对其进行了拷贝。熟悉C或者C艹的人这里应该很快能明白。

有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:懒皮

查看原文:Golang 学习笔记六 函数和方法的区别

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

622 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传