教女友写方法 -- 就要学习 Go 语言

这是『就要学习 Go 语言』系列的第 17 篇分享文章golang

刚接触 Go 语言的函数和方法时,我产生过这样的疑惑:为何会严格区分这二者的概念?学完以后才知道,不像别的语言(Java、PHP等)函数即方法,方法即函数,Go 语言中二者仍是有很大区别的。函数

方法定义

定义方法与函数相似,区别在于:方法定义时,在 func 和方法名之间会增长一个额外的参数。以下:学习

func (receiver Type) methodName(...Type) Type {
    ...
}
复制代码

(receiver Type) 是增长的额外参数,receiver 称为接收者,Type 能够是任意合法的类型,包括:结构体类型或新定义的类型。能够说,方法 methodName 属于类型 Type。方法名后面的参数和返回值是可选的。spa

type Employee struct {
	FirstName,LastName string
}

func (e Employee) fullName() string {
	return e.FirstName + " " + e.LastName
}

func main() {
	e := Employee{
		FirstName:"Jim",
		LastName:"Green",
	}
	fmt.Println(e.fullName())
}
复制代码

输出.net

Jim Green
复制代码

采用 Type.methodName(...) 语法调用类型的方法。 上面的代码,定义了一个不用传参、返回值为 string类型的方法 fullName。它属于结构体类型 Employee,咱们能够使用 e.fullName() 调用。fullName 中的 e 就是接收者,在方法内部能够访问结构体的每个成员。指针

如今,咱们应该很清楚,方法与函数的区别:方法属于某一种类型,且有接收者code

值接收者和指针接收者

到目前为止,建立的方法使用的都是值接收者,还能够经过下面的语法建立指针接收者的方法:cdn

func (receiver *Type) methodName(...Type) Type {
    ...
}
复制代码

值接收者和指针接收者,最大区别在于:在方法中修改指针接收者的值会影响到调用者的值,而值接收者就不会。一个是值的副本,一个是指针的副本,而指针的副本指向的仍是原来的值。对象

type Employee struct {
	FirstName,LastName string
	age int
}

func (e Employee)changeFirstName(name string)  {
	e.FirstName = name
	fmt.Println("changeFirstName",e)
}

func (e *Employee)changeAge(age int)  {
	e.age = age
}

func main() {

	e := Employee{
		FirstName:"Jim",
		LastName:"Green",
		age:30,
	}
	fmt.Println("changebefore",e)
	e.changeFirstName("firstName")
	fmt.Println("changeName",e)
	(&e).changeAge(18)
	fmt.Println("changeAge",e)
}
复制代码

输出内存

changebefore {Jim Green 30}
changeFirstName {firstName Green 30}
changeName {Jim Green 30}
changeAge {Jim Green 18}
复制代码

上面的代码,方法 changeFirstName() 使用的是值接收者,在方法中修改结构体的成员 FirstName 没有影响到原来的值;而方法 changeAge() 使用的是指针接收者,在方法中修改结构体成员 age,原来的值也被改变了。

不知道你有没有注意到,上面的代码中,调用指针接收者的方法时使用的是指针:(&e).changeAge(18) 。其实,平时编写代码的时候,能够写成:e.changeAge(18),编译器会自动帮咱们转成指针,以知足接收者的要求。同理,e.changeFirstName("firstName") 也能够写成 (&e).changeFirstName("firstName") ,但这样写就复杂,通常不这么作。

咱们应该考虑不一样的场景使用值接收者仍是指针接收者,若是在方法中发生的改变对调用者可见或者变量拷贝成本比较高的,就应该考虑使用指针接收者,其余状况建议使用值接收者。例如:大变量 A,占用内存大,使用值接收者的话拷贝成本高且效率低,这时就应该考虑使用指针接收者。

嵌套结构体的方法

咱们这里讲双层嵌套的结构体,外层称为父结构体,结构体成员称为子结构体,例如:

type Contact struct {
	phone,adress string
}
type Employee struct {
	FirstName,LastName string
	contact Contact
}
复制代码

Employee 是一个嵌套的结构体类型,称为父结构体,成员变量 contact 也是一个结构体,类型是 Contact,称为子结构体。

父结构体的方法,非匿名的成员结构体
type Contact struct {
	phone,adress string
}

type Employee struct {
	FirstName,LastName string
	contact Contact
}

func (e *Employee)changePhone(newPhone string){
	e.contact.phone = newPhone       // 注意访问方式
}

func main() {

	e := Employee{
		FirstName:"Jim",
		LastName:"Green",
		contact:Contact{
			phone:"111",
			adress:"HangZhou",
		},
	}
	fmt.Println("before:",e)
	e.changePhone("222")
	fmt.Println("after:",e)
}
复制代码

输出

before: {Jim Green {111 HangZhou}}
after: {Jim Green {222 HangZhou}}
复制代码

上面的代码,e 是嵌套结构体,在方法 changePhone() 中修改 contact 的成员 phone,注意修改的代码。

父结构体的方法,匿名的成员结构体
type Contact struct {
	phone,adress string
}

type Employee struct {
	FirstName,LastName string
	Contact
}

func (e *Employee)changePhone(newPhone string){
	// e.Contact.phone = newPhone // 方式一
	e.phone = newPhone				// 方式二
}

func main() {

	e := Employee{
		FirstName:"Jim",
		LastName:"Green",
		Contact:Contact{
			phone:"111",
			adress:"HangZhou",
		},
	}
	fmt.Println("before:",e)
	e.changePhone("222")
	fmt.Println("after:",e)
}
复制代码

输出结果与上面的同样。 上面的代码,Contact 是一个匿名成员结构体。在方法 changePhone() 中修改为员 phone,注意修改的两种方式。

子结构体的方法且非匿名
type Contact struct {
	phone,adress string
}

type Employee struct {
	FirstName,LastName string
	contact Contact
}

func (c *Contact)changePhone(newPhone string){
	c.phone = newPhone
}

func main() {

	e := Employee{
		FirstName:"Jim",
		LastName:"Green",
		contact:Contact{
			phone:"111",
			adress:"HangZhou",
		},
	}
	fmt.Println("before:",e)
	e.contact.changePhone("222")   // 注意调用方式,采用 .
	fmt.Println("after:",e)
}
复制代码

输出结果与上面的同样。 上面的代码,咱们基于结构体类型 Contact 建立了方法 changePhone(),在方法中修改为员 phone,注意调用方法的方式。

子结构体的方法且匿名
type Contact struct {
	phone,adress string
}

type Employee struct {
	FirstName,LastName string
	Contact
}

func (c *Contact)changePhone(newPhone string){
	c.phone = newPhone
}

func main() {

	e := Employee{
		FirstName:"Jim",
		LastName:"Green",
		Contact:Contact{
			phone:"111",
			adress:"HangZhou",
		},
	}
	fmt.Println(e)
	// e.Contact.changePhone("222") // 方式一
	e.changePhone("222")         	// 方式二
	fmt.Println(e)
}
复制代码

输出结果与上面的同样。 上面的代码,成员结构体 Contact 是匿名的,在方法 changePhone() 中修改为员 phone,注意调用方法的方式。

上面四个例子,但愿可以帮助你们更好理解嵌套结构体的方法!好,咱们接着往下。

非结构体类型的方法

目前为止,都是在结构体上定义方法。文章开始提到了,能够在 Go 任一合法类型上定义方法,可是,有个问题:必须保证类型和方法定义在同一个包里。以前,结构体和方法都定义在 main 包,因此能够运行。

package main
import "fmt"
func (i int)echo(){
	fmt.Println(i)
}

func main() {
	
}
复制代码

上面的代码,基于 int 类型建立了方法 echo(),因为 int 类型与方法 echo() 定义在不一样的包内,因此编译出错:cannot define new methods on non-local type int。 那如何解决呢?你可能会想到,在 main 包内建立 int 类型别名,对!就是这样:

package main
import "fmt"
type myInt int

func (i myInt) echo ()  {
	fmt.Println(i)
}

func main() {
	var a myInt
	a = 20
	a.echo()
}
复制代码

输出:20 上面的代码,基于类型别名 myInt 建立了方法 echo,保证了类型和方法都 main 包。

为什么须要方法

上面提到的例子,都是能够经过函数的方法实现的,回头想一想,Go 既然有了函数,为什么须要方法呢?

  • Go 不是纯粹的面向对象的语言且不支持类,经过类型的方法能够实现和类类似的功能,又不会像类那样显得很“重”;
  • 同名的方法能够定义在不一样的类型上,可是函数名不容许相同。
type Rect struct {
	width  int
	height int
}

type Circle struct {
	radius float64
}

func (r Rect) Area() int {
	return r.width * r.height
}

func (c Circle) Area() float64 {
	return math.Pi * c.radius * c.radius
}

func main() {
	rect := Rect{5, 4}
	cir := Circle{5.0}
	fmt.Printf("Rect Area %d\n", rect.Area())
	fmt.Printf("Circle Area %0.2f\n", cir.Area())
}
复制代码

输出

Rect Area 20
Circle Area 78.54
复制代码

上面的代码,在结构体 Rect 和 Circle 分别定义了同名的 Area() 方法,计算矩形和圆的面积。

学完这篇文章以后,相信你已经学会如何使用方法了,咱们下一节再见!


(全文完)

原创文章,若需转载请注明出处!
欢迎扫码关注公众号「Golang来啦」或者移步 seekload.net ,查看更多精彩文章。

公众号「Golang来啦」给你准备了一份神秘学习大礼包,后台回复【电子书】领取!

公众号二维码
相关文章
相关标签/搜索