方法简介 方法只是一个函数,在func关键字和方法名称之间有一个特殊的接收器类型。接收器可以是一个结构类型,也可以是非结构类型。
下面提供了方法声明的语法。
1 2 func (t Type) methodName(parameter list) { }
上面的片段创建了一个名为methodName的方法,其接收器类型为Type。t被称为接收器,它可以在方法中被访问。
方法示例 让我们写一个简单的程序,在一个结构类型上创建一个方法并调用它。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package mainimport ( "fmt" ) type Employee struct { name string salary int currency string } 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() }
在上述程序的第16行中,我们在Employee结构类型上创建了一个方法displaySalary。displaySalary()方法可以访问其内部的接收器e。在第17行,我们使用接收器e,并打印雇员的姓名、货币和工资。
在第26行中,我们使用语法emp1.displaySalary()调用该方法。
这个程序打印Salary of Sam Adolf is $5000。
方法与函数 上述程序可以只用函数而不用方法来重写。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package mainimport ( "fmt" ) type Employee struct { name string salary int currency string } func displaySalary (e Employee) { fmt.Printf("Salary of %s is %s%d" , e.name, e.currency, e.salary) } func main () { emp1 := Employee{ name: "Sam Adolf" , salary: 5000 , currency: "$" , } displaySalary(emp1) }
在上面的程序中,displaySalary方法被转换为一个函数,Employee结构被作为一个参数传递给它。这个程序也产生了完全相同的输出——Salary of Sam Adolf is $5000。
那么,既然我们可以用函数编写同样的程序,为什么还要用方法呢?这其中有几个原因。让我们逐一来看一下。
Go不是一种纯面向对象的编程语言,它不支持类。因此,类型上的方法是实现类似于类的行为的一种方式。方法允许对与类型相关的行为进行逻辑分组,类似于类。在上面的示例程序中,所有与Employee类型相关的行为都可以通过使用Employee接收器类型来创建方法进行分组。例如,我们可以添加诸如计算养老金、计算休假等方法。
同名的方法可以定义在不同的类型上,而同名的函数是不允许的。让我们假设我们有一个Square和Circle结构。我们可以在Square和Circle上都定义一个名为Area的方法。这在下面的程序中已经完成。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 package mainimport ( "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()) }
这个程序会打印出来:
1 2 Area of rectangle 50 Area of circle 452.389342
上述方法的属性是用来实现接口的。我们将在下一个教程中处理接口时详细讨论这个问题。
指针接收器与值接收器 到目前为止,我们只看到了带有值接收器的方法。我们也可以创建带有指针接收器的方法。值接收器和指针接收器之间的区别是,在一个带有指针接收器的方法中所作的改变对调用者来说是可见的,而在值接收器中则不是这样的。让我们借助于一个程序来理解这一点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package mainimport ( "fmt" ) type Employee struct { name string age int } func (e Employee) changeName(newName string ) { e.name = newName } 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结构的名称字段所做的修改对调用者来说是不可见的,因此程序在第32行调用e.changeName("Michael Andrew")方法之前和之后都打印了相同的名称。由于changeAge方法有一个指针接收器(e *Employee),在调用方法(&e).changeAge(51)之后,age字段的变化将对调用者可见。这个程序会打印:
1 2 3 4 5 Employee name before change: Mark Andrew Employee name after change: Mark Andrew Employee age before change: 50 Employee age after change: 51
在上述程序的第36行中,我们使用(&e).changeAge(51)来调用changeAge方法。由于changeAge有一个指针接收器,我们使用了(&e)来调用该方法。这是不需要的,语言让我们选择直接使用e.changeAge(51)。e.changeAge(51)将被Go语言解释为(&e).changeAge(51)。
下面的程序被改写为使用e.changeAge(51)而不是(&e).changeAge(51),它打印出相同的输出。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package mainimport ( "fmt" ) type Employee struct { name string age int } func (e Employee) changeName(newName string ) { e.name = newName } 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) }
何时使用指针接收器,何时使用值接收器 一般来说,当方法内部对接收器的改变对调用者来说应该是可见的,就可以使用指针接收器。
指针接收器也可以用在那些复制数据结构成本很高的地方。考虑一个有许多字段的结构。在方法中使用这个结构作为一个值接收器,需要复制整个结构,这将是很昂贵的。在这种情况下,如果使用一个指针接收器,该结构将不会被复制,在方法中只使用它的一个指针。
在所有其他情况下,可以使用值接收器。
匿名结构字段的方法 属于结构的匿名字段的方法可以被调用,就像它们属于定义匿名字段的结构一样。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 package mainimport ( "fmt" ) type address struct { city string state string } func (a address) fullAddress() { fmt.Printf("Full address: %s, %s" , a.city, a.state) } type person struct { firstName string lastName string address } func main () { p := person{ firstName: "Elon" , lastName: "Musk" , address: address { city: "Los Angeles" , state: "California" , }, } p.fullAddress() }
在上述程序的第32行,我们用p.fullAddress()调用address结构的fullAddress()方法。在第32行中,我们使用p.fullAddress()调用address结构的fullAddress()方法。明确的方向p.address.fullAddress()是不需要的。这个程序打印出
1 Full address: Los Angeles, California
方法中的值接收方与函数中的值参数 这个话题让大多数新手头疼不已。我将尽量把它说清楚😀。
当一个函数有一个值参数时,它将只接受一个值参数。
当一个方法有一个值接收器时,它将同时接受指针和值接收器。
让我们通过一个例子来理解这一点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 package mainimport ( "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 p.area() }
第12行的函数func area(r rectangle)接受一个值参数,第16行的方法func (r rectangle) area()接受一个值的接收器。
在第25行,我们用一个值参数调用area(r)函数,它将工作。同样,我们用一个值接收器调用 area 方法 r.area(),这也会起作用。
我们在第28行创建一个指向r的指针p。如果我们试图将这个指针传递给只接受一个值的函数 area,编译器会报错。我对第33行做了注释。如果你不注释这一行,那么编译器将抛出编译错误,不能将p(type *rectangle)作为矩形类型的参数传给 area。这和预期的一样。
现在棘手的部分来了,第35行的代码p.area()调用了方法area,它只接受一个使用指针接收器p的值接收器,这是完全有效的。原因是p.area()这一行,为了方便起见,将被Go解释为(*p).area(),因为 area 有一个值接收器。
1 2 3 Area Function result: 50 Area Method result: 50 Area Method result: 50
方法中的指针接收器与函数中的指针参数 与值参数类似,带有指针参数的函数将只接受指针,而带有指针接收器的方法将同时接受指针和值接收器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 package mainimport ( "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 perimeter(p) p.perimeter() r.perimeter() }
上述程序的第12行定义了一个接受指针参数的perimeter函数,第17行定义了一个具有指针接收器的方法。
在第27行,我们用一个指针参数调用perimeter函数,在第28行,我们用一个指针接收器调用perimeter方法。一切都很好。
在第33行的注释中,我们试图用一个值参数r来调用perimeter函数。这是不允许的,因为一个有指针参数的函数是不接受值参数的。如果取消这一行并运行程序,编译将失败,错误是main.go:33: cannot use r (type rectangle) as type *rectangle in argument to perimeter 。
在第35行中,我们用一个值接收器r调用指针接收器方法perimeter,这是允许的,为了方便起见,这行代码r.perimeter()将被语言解释为(&r).perimeter()。这个程序将输出。
1 2 3 perimeter function output: 30 perimeter method output: 30 perimeter method output: 30
具有非结构接收器的方法 到目前为止,我们只在结构类型上定义了方法。也可以在非结构类型上定义方法,但有一个问题。要在一个类型上定义方法,接收器类型的定义和方法的定义应该存在于同一个包中。到目前为止,我们定义的所有结构和结构上的方法都位于同一个main包中,因此它们都能工作。
1 2 3 4 5 6 7 8 package mainfunc (a int ) add(b int ) { } func main () {}
在上面的程序中,在第3行,我们试图在内置类型int上添加一个名为add的方法。这是不允许的,因为add方法的定义和int类型的定义不在同一个包里。这个程序将抛出编译错误不能在非本地类型int上定义新方法。
让这个程序正常工作的方法是为内置类型int创建一个类型别名,然后用这个类型别名创建一个方法作为接收器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package mainimport "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) }
在上述程序的第5行,我们已经为int创建了一个类型别名myInt。在第7行中,我们定义了一个以myInt为接收器的add方法。
这个程序将打印Sum is 15。
Go中的方法就介绍到这里。祝你有个愉快的一天。