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

栏目: Go · 发布时间: 5年前

内容简介:在接触到go之前,我认为函数和方法只是同一个东西的两个名字而已(在我熟悉的c/c++,python,java中没有明显的区别),但是在golang中者完全是两个不同的东西。官方的解释是,方法是包含了接收者的函数。1.定义函数声明包括函数名、形式参数列表、返回值列表( 可省略) 以及函数体。

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

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

开发高质量PHP框架与应用的实际案例解析

开发高质量PHP框架与应用的实际案例解析

Sebastian Bergmann / 刘文瀚、刘海燕 / 清华大学出版社 / 2012-6 / 49.00元

PHP已经成为最受欢迎的编程语言之一,这使得用PHP创建高质量、易维护的应用程序和框架比以往受到更多的青睐。通过使用来自于知名公司的真实案例研究,《开发高质量PHP框架与应用的实际案例解析》为Web软件体系结构的不同层次介绍了规划、执行以及测试自动化方面的内容,并解释了这些公司如何测量和测试软件质量。《开发高质量PHP框架与应用的实际案例解析》作者Sebastian Bergmann、Stefan......一起来看看 《开发高质量PHP框架与应用的实际案例解析》 这本书的介绍吧!

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

html转js在线工具
html转js在线工具

html转js在线工具