Go中的struct结构相似于面向对象中的类。面向对象中,除了成员变量还有方法。数据结构
Go中也有方法,它是一种特殊的函数,定义于struct之上(与struct关联、绑定),被称为struct的receiver。函数
它的定义方式大体以下:this
type mytype struct{} func (recv mytype) my_method(para) return_type {} func (recv *mytype) my_method(para) return_type {}
这表示my_method()
函数是绑定在mytype这个struct type上的,是与之关联的,是独属于mytype的。因此,此函数称为"方法"。因此,方法和字段同样,也是struct类型的一种属性。指针
其中方法名前面的(recv mytype)
或(recv *mytype)
是方法的receiver,具备了receiver的函数才能称之为方法,它将函数和type进行了关联,使得函数绑定到type上。至于receiver的类型是mytype
仍是*mytype
,后面详细解释。code
定义了属于mytype的方法以后,就能够直接经过mytype来调用这个方法:对象
mytype.my_method()
来个实际的例子,定义一个名为changfangxing的struct类型,属性为长和宽,定义属于changfangxing的求面积的方法area()。继承
package main import "fmt" type changfangxing struct { length float64 width float64 } func (c *changfangxing) area() float64 { return c.length * c.width } func main() { c := &changfangxing{ 2.5, 4.0, } fmt.Printf("%f\n",c.area()) }
1.方法的receiver type并不是必定要是struct类型,type定义的类型别名、slice、map、channel、func类型等均可以。但内置简单数据类型(int、float等)不行,interface类型不行。递归
package main import "fmt" type myint int func (i *myint) numadd(n int) int { return n + 1 } func main() { n := new(myint) fmt.Println(n.numadd(4)) }
以slice为类型,定义属于它的方法:字符串
package main import "fmt" type myslice []int func (v myslice) sumOfSlice() int { sum := 0 for _, value := range v { sum += value } return sum } func main() { s := myslice{11, 22, 33} fmt.Println(s.sumOfSlice()) }
2.struct结合它的方法就等价于面向对象中的类。只不过struct能够和它的方法分开,并不是必定要属于同一个文件,但必须属于同一个包。因此,没有办法直接在int、float等内置的简单类型上定义方法,真要为它们定义方法,能够像上面示例中同样使用type定义这些类型的别名,而后定义别名的方法。get
3.方法有两种类型:(T Type)
和(T *Type)
,它们之间有区别,后文解释。
4.方法就是函数,因此Go中没有方法重载(overload)的说法,也就是说同一个类型中的全部方法名必须都惟一。但不一样类型中的方法,能够重名。例如:
func (a *mytype1) add() ret_type {} func (a *mytype2) add() ret_type {}
5.type定义类型的别名时,别名类型不会拥有原始类型的方法。例如mytype上定义了方法add(),mytype的别名new_type不会有这个方法,除非本身从新定义。
6.若是receiver是一个指针类型,则会自动解除引用。例如,下面的a是指针,它会自动解除引用使得能直接调用属于mytype1实例的方法add()。
func (a *mytype1) add() ret_type {} a.add()
7.(T Type)
或(T *Type)
的T,其实就是面向对象语言中的this或self,表示调用该实例的方法。若是愿意,天然可使用self或this,例如(self Type)
,但这是能够随意的。
8.方法和type是分开的,意味着实例的行为(behavior)和数据存储(field)是分开的,可是它们经过receiver创建起关联关系。
其实方法本质上就是函数,但方法是关联了类型的,能够直接经过类型的实例去调用属于该实例的方法。
例如,有一个type person,若是定义它的方法setname()和定义通用的函数setname2(),它们要实现相同的为person赋值名称时,参数不同:
func (p *person) setname(name string) { p.name = name } func setname2(p *person,name string) { p.name = name }
经过函数为person的name赋值,必须将person的实例做为函数的参数之一,而经过方法则无需声明这个额外的参数,由于方法是关联到person实例的。
假若有一个person struct:
type person struct{ name string age int }
有两种类型的实例:
p1 := new(person) p2 := person{}
p1是指针类型的person实例,p2是值类型的person实例。虽然p1是指针,但它也是实例。在须要访问或调用person实例属性时候,若是发现它是一个指针类型的变量,Go会自动将其解除引用,因此p1.name
在内部其实是(*p1).name
。同理,调用实例的方法时也同样,有须要的时候会自动解除引用。
除了实例有值类型和指针类型的区别,方法也有值类型的方法和指针类型的区别,也就是如下两种receiver:
func (p person) setname(name string) { p.name = name } func (p *person) setage(age int) { p.age = age }
setname()方法中是值类型的receiver,setage()方法中是指针类型的receiver。它们是有区别的。
首先,setage()方法的p是一个指针类型的person实例,因此方法体中的p.age
实际上等价于(*p).age
。
再者,方法就是函数,Go中全部须要传值的时候,都是按值传递的,也就是拷贝一个副本。
setname()中,除了参数name string
须要拷贝,receiver部分(p person)
也会拷贝,并且它明确了要拷贝的对象是值类型的实例,也就是拷贝完整的person数据结构。但实例有两种类型:值类型和指针类型。(p person)
无视它们的类型,由于receiver严格规定p是一个值类型的实例。因此不管是指针类型的p1实例仍是值类型的p2实例,都会拷贝整个实例对象。对于指针类型的实例p1,前面说了,在须要的时候,Go会自动解除引用,因此p1.setname()
等价于(*p1).setname()
。
也就是说,只要receiver是值类型的,不管是使用值类型的实例仍是指针类型的实例,都是拷贝整个底层数据结构的,方法内部访问的和修改的都是实例的副本。因此,若是有修改操做,不会影响外部原始实例。
setage()中,receiver部分(p *person)
明确指定了要拷贝的对象是指针类型的实例,不管是指针类型的实例p1仍是值类型的p2,都是拷贝指针。因此p2.setage()
等价于(&p2).setage()
。
也就是说,只要receiver是指针类型的,不管是使用值类型的实例仍是指针类型的实例,都是拷贝指针,方法内部访问的和修改的都是原始的实例数据结构。因此,若是有修改操做,会影响外部原始实例。
那么选择值类型的receiver仍是指针类型的receiver?通常来讲选择指针类型的receiver。
下面的代码解释了上面的结论:
package main import "fmt" type person struct { name string age int } func (p person) setname(name string) { p.name = name } func (p *person) setage(age int) { p.age = age } func (p *person) getname() string { return p.name } func (p *person) getage() int { return p.age } func main() { // 指针类型的实例 p1 := new(person) p1.setname("longshuai1") p1.setage(21) fmt.Println(p1.getname()) // 输出"" fmt.Println(p1.getage()) // 输出21 // 值类型的实例 p2 := person{} p2.setname("longshuai2") p2.setage(23) fmt.Println(p2.getname()) // 输出"" fmt.Println(p2.getage()) // 输出23 }
上面分别建立了指针类型的实例p1和值类型的实例p2,但不管是p1仍是p2,它们调用setname()方法设置的name值都没有影响原始实例中的name值,因此getname()都输出空字符串,而它们调用setage()方法设置的age值都影响了原始实例中的age值。
当内部struct嵌套进外部struct时,内部struct的方法也会被嵌套,也就是说外部struct拥有了内部struct的方法。
例如:
package main import ( "fmt" ) type person struct{} func (p *person) speak() { fmt.Println("speak in person") } // Admin exported type Admin struct { person a int } func main() { a := new(Admin) // 直接调用内部struct的方法 a.speak() // 间接调用内部stuct的方法 a.person.speak() }
当person被嵌套到Admin中后,Admin就拥有了person中的属性,包括方法speak()。因此,a.speak()
和a.person.speak()
都是可行的。
若是Admin也有一个名为speak()的方法,那么Admin的speak()方法将掩盖内部struct的person的speak()方法。因此a.speak()
调用的将是属于Admin的speak(),而a.preson.speak()
将调用的是person的speak()。
验证以下:
func (a *Admin) speak() { fmt.Println("speak in Admin") } func main() { a := new(Admin) // 直接调用内部struct的方法 a.speak() // 间接调用内部stuct的方法 a.person.speak() }
输出结果为:
speak in Admin speak in person
除了能够经过嵌套的方式获取内部struct的方法,还有一种方式能够获取另外一个struct中的方法:将另外一个struct做为外部struct的一个命名字段。
例如:
type person struct { name string age int } type Admin struct { people *person salary int }
如今Admin除了本身的salary属性,还指向一个person。这和struct嵌套不同,struct嵌套是直接外部包含内部,而这种组合方式是一个struct指向另外一个struct,从Admin能够追踪到其指向的person。因此,它更像是链表。
例如,person是Admin type中的一个字段,person有方法speak()。
package main import ( "fmt" ) type person struct { name string age int } type Admin struct { people *person salary int } func main() { // 构建Admin实例 a := new(Admin) a.salary = 2300 a.people = new(person) a.people.name = "longshuai" a.people.age = 23 // 或a := &Admin{&person{"longshuai",23},2300} // 调用属于person的方法speak() a.people.speak() } func (p *person) speak() { fmt.Println("speak in person") }
或者,定义一个属于Admin的方法,在此方法中应用person的方法:
func (a *Admin) sing(){ a.people.speak() }
而后只需调用a.sing()
就能够隐藏person的方法。
由于Go的struct支持嵌套多个其它匿名字段,因此支持"多重继承"。这意味着外部struct能够从多个内部struct中获取属性、方法。
例如,照相手机cameraPhone是一个struct,其内嵌套Phone和Camera两个struct,那么cameraPhone就能够获取来自Phone的call()方法进行拨号通话,获取来自Camera()的takeAPic()方法进行拍照。
面向对象的语言都强烈建议不要使用多重继承,甚至有些语言本就不支持多重继承。至于Go是否要使用"多重继承",看需求了,没那么多限制。
fmt包中的Println()、Print()和Printf()的%v
都会自动调用String()方法将待输出的内容进行转换。
能够在本身的struct上重写String()方法,使得输出这个示例的时候,就会调用它本身的String()。
例如,定义person的String(),它将person中的name和age结合起来:
package main import ( "fmt" "strconv" ) type person struct { name string age int } func (p *person) String() string { return p.name + ": " + strconv.Itoa(p.age) } func main() { p := new(person) p.name = "longshuai" p.age = 23 // 输出person的实例p,将调用String() fmt.Println(p) }
上面将输出:
longshuai: 23
必定要注意,定义struct的String()方法时,String()方法里不要出现fmt.Print()、fmt.Println以及fmt.Printf()的%v
,由于它们自身会调用String(),会出现无限递归的问题。