一 概述
对于面向对象编程的支持Go 语言设计得非常简洁而优雅。因为, Go语言并没有沿袭传统面向对象编程中的诸多概念,比如继承(不支持继承,尽管匿名字段的内存布局和行为类似继承,但它并不是继承)、虚函数、构造函数和析构函数、隐藏的this指针等。
尽管Go语言中没有封装、继承、多态这些概念,但同样通过别的方式实现这些特性:
封装:通过方法实现
继承:通过匿名字段实现
多态:通过接口实现
二 匿名组合
2.1 匿名字段
一般情况下,定义结构体的时候是字段名与其类型一一对应,实际上Go支持只提供类型,而不写字段名的方式,也就是匿名字段,也称为嵌入字段。
当匿名字段也是一个结构体的时候,那么这个结构体所拥有的全部字段都被隐式地引入了当前定义的这个结构体。
type Person struct { name string sex byte age int } type Student struct { Person id int addr string }
2.2 初始化
package mainimport ( "fmt" ) type Person struct { name string sex byte age int } type Student struct { Person id int addr string } func main () { s1 := Student{Person{"mike" , 'm' , 18 }, 1 , "sz" } fmt.Printf("s1 = %+v\n" , s1) s3 := Student{Person: Person{"lily" , 'f' , 19 }, id: 2 } fmt.Printf("s3 = %+v\n" , s3) s4 := Student{Person: Person{name: "tom" }, id: 3 } fmt.Printf("s4 = %+v\n" , s4) }
运行结果为
s1 = {Person: {name: mike sex: 109 age: 18 } id: 1 addr: sz} s3 = {Person: {name: lily sex: 102 age: 19 } id: 2 addr: } s4 = {Person: {name: tom sex: 0 age: 0 } id: 3 addr: }
2.3 成员的操作
package mainimport ( "fmt" ) type Person struct { name string sex byte age int } type Student struct { Person id int addr string } func main () { var s1 Student s1.name = "mike" s1.sex = 'm' s1.age = 18 s1.id = 1 s1.addr = "sz" fmt.Println(s1) var s2 Student s2.Person = Person{"lily" , 'f' , 19 } s2.id = 2 s2.addr = "bj" fmt.Println(s2) }
运行结果为
{{mike 109 18 } 1 sz} {{lily 102 19 } 2 bj}
2.4 同名字段
package mainimport ( "fmt" ) type Person struct { name string sex byte age int } type Student struct { Person id int addr string name string } func main () { var s Student s.name = "mike" fmt.Printf("%+v\n" , s) s.Person.name = "yoyo" fmt.Printf("%+v\n" , s) }
运行结果为
{Person: {name: sex: 0 age: 0 } id: 0 addr: name: mike} {Person: {name: yoyo sex: 0 age: 0 } id: 0 addr: name: mike}
2.5 其它匿名字段
2.5.1 非结构体类型
所有的内置类型和自定义类型都是可以作为匿名字段的:
package mainimport ( "fmt" ) type mystr string type Person struct { name string sex byte age int } type Student struct { Person int mystr } func main () { s1 := Student{Person{"mike" , 'm' , 18 }, 1 , "bj" } fmt.Printf("%+v\n" , s1) fmt.Printf("%s, %c, %d, %d, %s\n" , s1.name, s1.sex, s1.age, s1.int , s1.mystr) }
运行结果为
{Person :{name:mike sex:109 age:18 } int :1 mystr :bj } mike , m , 18, 1, bj
2.5.2 结构体指针类型
package mainimport ( "fmt" ) type Person struct { name string sex byte age int } type Student struct { *Person id int addr string } func main () { s1 := Student{&Person{"mike" , 'm' , 18 }, 1 , "bj" } fmt.Printf("%+v\n" , s1) fmt.Printf("%s, %c, %d\n" , s1.name, s1.sex, s1.age) var s2 Student s2.Person = new (Person) s2.name = "yoyo" s2.sex = 'f' s2.age = 20 s2.id = 2 s2.addr = "sz" fmt.Println(s2.name, s2.sex, s2.age, s2.id, s2.age) }
运行结果为
{Person:0xc000004480 id:1 addr:bj} mike, m, 18 yoyo 102 20 2 20
三 方法
3.1 概述
在面向对象编程中,一个对象其实也就是一个简单的值或者一个变量,在这个对象中会包含一些函数,这种带有接收者的函数,我们称为方法(method
)。 本质上,一个方法则是一个和特殊类型关联的函数。
一个面向对象的程序会用方法来表达其属性和对应的操作,这样使用这个对象的用户就不需要直接去操作对象,而是借助方法来做这些事情。
在Go语言中,可以给任意自定义类型(包括内置类型,但不包括指针类型 )添加相应的方法。
一个方法就是一个包含了接受者的函数,接受者可以是命名类型或者结构体类型的一个值或者是一个指针。所有给定类型的方法属于该类型的方法集
⽅法总是绑定对象实例,并隐式将实例作为第⼀实参 (receiver
),方法的语法如下:
func (receiver ReceiverType) funcName(parameters ) (results )
参数 receiver 可任意命名。如⽅法中未曾使⽤,可省略参数名。
参数 receiver 类型可以是 T 或 *T。基类型 T 不能是接⼝或指针。
不支持重载方法,也就是说,不能定义名字相同但是不同参数的方法。
3.2 为类型添加方法
3.2.1 基础类型作为接收者
package mainimport ( "fmt" ) type MyInt int func (a MyInt) Add (b MyInt) MyInt { return a + b } func Add (a, b MyInt) MyInt { return a + b } func main () { var a MyInt = 1 var b MyInt = 1 fmt.Println("a.Add(b) = " , a.Add(b)) fmt.Println("Add(a, b) = " , Add(a, b)) }
运行结果为
a.Add (b) = 2 Add (a, b) = 2
通过上面的例子可以看出,面向对象只是换了一种语法形式来表达。方法是函数的语法糖,因为receiver其实就是方法所接收的第1个参数 。
注意:虽然方法的名字一模一样,但是如果接收者不一样,那么方法就不一样。
3.2.2 结构体作为接收者
方法里面可以访问接收者的字段,调用方法通过点( .
)访问,就像struct里面访问字段一样:
package mainimport ( "fmt" ) type Person struct { name string sex byte age int } func (p Person) PrintInfo () { fmt.Println(p.name, p.sex, p.age) } func main () { p := Person{"mike" , 'm' , 18 } p.PrintInfo() }
运行结果为
3.3 值语义和引用语义
package mainimport ( "fmt" ) type Person struct { name string sex byte age int } func (p *Person) SetInfoPointer () { (*p).name = "yoyo" p.sex = 'f' p.age = 22 } func (p Person) SetInfoValue () { p.name = "yoyo" p.sex = 'f' p.age = 22 } func main () { p1 := Person{"mike" , 'm' , 18 } fmt.Println("函数调用前 = " , p1) (&p1).SetInfoPointer() fmt.Println("函数调用后 = " , p1) fmt.Println("==========================" ) p2 := Person{"mike" , 'm' , 18 } fmt.Println("函数调用前 = " , p2) p2.SetInfoValue() fmt.Println("函数调用后 = " , p2) }
运行结果为
函数调用前 = {mike 109 18} 函数调用后 = {yoyo 102 22} ========================== 函数调用前 = {mike 109 18} 函数调用后 = {mike 109 18}
3.4 方法集
类型的方法集是指可以被该类型的值调用的所有方法的集合。
用实例实例 value
和 pointer
调用方法(含匿名字段)不受⽅法集约束,编译器编总是查找全部方法,并自动转换 receiver
实参。
3.4.1 类型 *T 方法集
一个指向自定义类型的值的指针,它的方法集由该类型定义的所有方法组成,无论这些方法接受的是一个值还是一个指针。
如果在指针上调用一个接受值的方法,Go语言会聪明地将该指针解引用,并将指针所指的底层值作为方法的接收者。
类型 *T
⽅法集包含全部 receiver T + *T
⽅法:
package main import ( _ "fmt" ) type Person struct { name string sex byte age int } //指针作为接收者,引用语义 func (p *Person) SetInfoPointer () { (*p).name = "yoyo" p.sex = 'f' p.age = 22 } //值作为接收者,值语义 func (p Person) SetInfoValue () { p.name = "xxx" p.sex = 'm' p.age = 33 } func main () { //p 为指针类型 var p *Person = &Person{"mike" , 'm' , 18 } p.SetInfoPointer() //func (p) SetInfoPointer () p.SetInfoValue() //func (*p) SetInfoValue () (*p).SetInfoValue() //func (*p) SetInfoValue () }
3.4.2 类型 T 方法集
一个自定义类型值的方法集则由为该类型定义的接收者类型为值类型的方法组成,但是不包含那些接收者类型为指针的方法。
但这种限制通常并不像这里所说的那样,因为如果我们只有一个值,仍然可以调用一个接收者为指针类型的方法,这可以借助于Go语言传值的地址能力实现。
package main import ( _ "fmt" ) type Person struct { name string sex byte age int } //指针作为接收者,引用语义 func (p *Person) SetInfoPointer () { (*p).name = "yoyo" p.sex = 'f' p.age = 22 } //值作为接收者,值语义 func (p Person) SetInfoValue () { p.name = "xxx" p.sex = 'm' p.age = 33 } func main () { //p 为普通值类型 var p Person = Person{"mike" , 'm' , 18 } (&p).SetInfoPointer() //func (&p) SetInfoPointer () p.SetInfoPointer() //func (&p) SetInfoPointer () p.SetInfoValue() //func (p) SetInfoValue () (&p).SetInfoValue() //func (*&p) SetInfoValue () }
3.5 匿名字段
3.5.1 方法的继承
如果匿名字段实现了一个方法,那么包含这个匿名字段的struct
也能调用该方法。
package mainimport ( _ "fmt" ) type Person struct { name string sex byte age int } func (p *Person) PrintInfo () { fmt.Printf("%s,%c,%d\n" , p.name, p.sex, p.age) } type Student struct { Person id int addr string } func main () { p := Person{"mike" , 'm' , 18 } p.PrintInfo() s := Student{Person{"yoyo" , 'f' , 20 }, 2 , "sz" } s.PrintInfo() }
3.5.2 方法的重写
package mainimport ( _ "fmt" ) type Person struct { name string sex byte age int } func (p *Person) PrintInfo () { fmt.Printf("Person: %s,%c,%d\n" , p.name, p.sex, p.age) } type Student struct { Person id int addr string } func (s *Student) PrintInfo () { fmt.Printf("Student:%s,%c,%d\n" , s.name, s.sex, s.age) } func main () { p := Person{"mike" , 'm' , 18 } p.PrintInfo() s := Student{Person{"yoyo" , 'f' , 20 }, 2 , "sz" } s.PrintInfo() s.Person.PrintInfo() }
3.6 表达式
类似于我们可以对函数进行赋值和传递一样,方法也可以进行赋值和传递。
根据调用者不同,方法分为两种表现形式:方法值和方法表达式。两者都可像普通函数那样赋值和传参,区别在于方法值绑定实例,⽽方法表达式则须显式传参。
3.6.1 方法值
package mainimport ( "fmt" ) type Person struct { name string sex byte age int } func (p *Person) PrintInfoPointer () { fmt.Printf("%p, %v\n" , p, p) } func (p Person) PrintInfoValue () { fmt.Printf("%p, %v\n" , &p, p) } func main () { p := Person{"mike" , 'm' , 18 } p.PrintInfoPointer() pFunc1 := p.PrintInfoPointer pFunc1() pFunc2 := p.PrintInfoValue pFunc2() }
运行结果为
0xc000092000 , &{mike 109 18 }0xc000092000 , &{mike 109 18 }0xc000092080 , {mike 109 18 }
3.6.2 方法表达式
package mainimport ( "fmt" ) type Person struct { name string sex byte age int } func (p *Person) PrintInfoPointer () { fmt.Printf("%p, %v\n" , p, p) } func (p Person) PrintInfoValue () { fmt.Printf("%p, %v\n" , &p, p) } func main () { p := Person{"mike" , 'm' , 18 } p.PrintInfoPointer() pFunc1 := (*Person).PrintInfoPointer pFunc1(&p) pFunc2 := Person.PrintInfoValue pFunc2(p) }
运行结果为
0xc000098420 , &{mike 109 18 }0xc000098420 , &{mike 109 18 }0xc0000984a0 , {mike 109 18 }
四 接口
4.1 概述
在Go语言中,接口(interface
)是一个自定义类型,接口类型具体描述了一系列方法的集合。
接口类型是一种抽象的类型,它不会暴露出它所代表的对象的内部值的结构和这个对象支持的基础操作的集合,它们只会展示出它们自己的方法。因此接口类型不能将其实例化。
Go通过接口实现了鸭子类型(duck-typing):“当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子”。我们并不关心对象是什么类型,到底是不是鸭子,只关心行为。
4.2 接口的使用
4.2.1 接口定义
type Humaner interface { SayHi() }
接⼝命名习惯以 er
结尾
接口只有方法声明,没有实现,没有数据字段
接口可以匿名嵌入其它接口,或嵌入到结构中
4.2.2 接口实现
接口是用来定义行为的类型。这些被定义的行为不由接口直接实现,而是通过方法由用户定义的类型实现,一个实现了这些方法的具体类型是这个接口类型的实例。
如果用户定义的类型实现了某个接口类型声明的一组方法,那么这个用户定义的类型的值就可以赋给这个接口类型的值。这个赋值会把用户定义的类型的值存入接口类型的值。
package mainimport ( "fmt" ) type Humaner interface { SayHi() } type Student struct { name string score float64 } func (s *Student) SayHi () { fmt.Printf("Student[%s, %f] say hi!!\n" , s.name, s.score) } type Teacher struct { name string group string } func (t *Teacher) SayHi () { fmt.Printf("Teacher[%s, %s] say hi!!\n" , t.name, t.group) } type MyStr string func (str MyStr) SayHi () { fmt.Printf("MyStr[%s] say hi!!\n" , str) } func WhoSayHi (i Humaner) { i.SayHi() } func main () { s := &Student{"mike" , 88.88 } t := &Teacher{"yoyo" , "Go语言" } var tmp MyStr = "测试" s.SayHi() t.SayHi() tmp.SayHi() WhoSayHi(s) WhoSayHi(t) WhoSayHi(tmp) x := make ([]Humaner, 3 ) x[0 ], x[1 ], x[2 ] = s, t, tmp for _, value := range x { value.SayHi() } }
运行结果为
Student[mike, 88.880000 ] say hi!! Teacher[yoyo, Go语言] say hi!! MyStr[测试] say hi!! Student[mike, 88.880000 ] say hi!! Teacher[yoyo, Go语言] say hi!! MyStr[测试] say hi!! Student[mike, 88.880000 ] say hi!! Teacher[yoyo, Go语言] say hi!! MyStr[测试] say hi!!
通过上面的代码,你会发现接口就是一组抽象方法的集合,它必须由其他非接口类型实现,而不能自我实现。
4.3 接口组合
4.3.1 接口嵌入
如果一个interface1
作为interface2
的一个嵌入字段,那么interface2
隐式的包含了interface1
里面的方法。
package mainimport ( "fmt" ) type Humaner interface { SayHi() } type Personer interface { Humaner Sing(lyrics string ) } type Student struct { name string score float64 } func (s *Student) SayHi () { fmt.Printf("Student[%s, %f] say hi!!\n" , s.name, s.score) } func (s *Student) Sing (lyrics string ) { fmt.Printf("Student sing[%s]!!\n" , lyrics) } func main () { s := &Student{"mike" , 88.88 } var i2 Personer i2 = s i2.SayHi() i2.Sing("学生哥" ) }
运行结果为
Student[mike, 88.880000 ] say hi!! Student sing[学生哥]!!
4.3.2 接口转换
超集接⼝对象可转换为⼦集接⼝,反之出错:
package mainimport ( "fmt" ) type Humaner interface { SayHi() } type Personer interface { Humaner Sing(lyrics string ) } type Student struct { name string score float64 } func (s *Student) SayHi () { fmt.Printf("Student[%s, %f] say hi!!\n" , s.name, s.score) } func (s *Student) Sing (lyrics string ) { fmt.Printf("Student sing[%s]!!\n" , lyrics) } func main () { var i1 Personer = &Student{"mike" , 88.88 } var i2 Humaner = i1 i2.SayHi() }
运行结果为
Student[mike, 88.880000 ] say hi!!
4.4 空接口
空接口(interface{}
)不包含任何的方法,正因为如此,所有的类型都实现了空接口,因此空接口可以存储任意类型的数值。它有点类似于C语言的void *
类型。
var v1 interface { } = 1 // 将int类型赋值给interface { } var v2 interface { } = "abc" // 将string类型赋值给interface { } var v3 interface { } = &v2 // 将*interface { }类型赋值给interface { } var v4 interface { } = struct { X int }{1 } var v5 interface { } = &struct { X int }{1 }
当函数可以接受任意的对象实例时,我们会将其声明为interface{}
,最典型的例子是标准库fmt
中PrintXXX
系列的函数,例如:
func Printf (fmt string , args ...interface {}) func Println (args ...interface {})
4.5 类型查询
我们知道interface
的变量里面可以存储任意类型的数值(该类型实现了interface
)。那么我们怎么反向知道这个变量里面实际保存了的是哪个类型的对象呢?目前常用的有两种方法:
4.5.1 comma-ok断言
Go语言里面有一个语法,可以直接判断是否是该类型的变量: value, ok = element.(T)
,这里value
就是变量的值,ok
是一个bool
类型,element
是interface
变量,T
是断言的类型。
如果element
里面确实存储了T
类型的数值,那么ok
返回true
,否则返回false
。
示例代码:
package mainimport ( "fmt" ) type Element interface {}type Person struct { name string age int } func main () { list := make ([]Element, 3 ) list[0 ] = 1 list[1 ] = "Hello" list[2 ] = Person{"mike" , 18 } for index, element := range list { if value, ok := element.(int ); ok { fmt.Printf("list[%d] is an int and its value is %d\n" , index, value) } else if value, ok := element.(string ); ok { fmt.Printf("list[%d] is a string and its value is %s\n" , index, value) } else if value, ok := element.(Person); ok { fmt.Printf("list[%d] is a Person and its value is [%s, %d]\n" , index, value.name, value.age) } else { fmt.Printf("list[%d] is of a different type\n" , index) } } }
运行结果为
list [0 ] is an int and its value is 1 list [1 ] is a string and its value is Hellolist [2 ] is a Person and its value is [mike, 18 ]
4.5.2 switch测试
package mainimport ( "fmt" ) type Element interface {}type Person struct { name string age int } func main () { list := make ([]Element, 3 ) list[0 ] = 1 list[1 ] = "Hello" list[2 ] = Person{"mike" , 18 } for index, element := range list { switch value := element.(type ) { case int : fmt.Printf("list[%d] is an int and its value is %d\n" , index, value) case string : fmt.Printf("list[%d] is a string and its value is %s\n" , index, value) case Person: fmt.Printf("list[%d] is a Person and its value is [%s, %d]\n" , index, value.name, value.age) default : fmt.Println("list[%d] is of a different type" , index) } } }
运行结果为
list [0 ] is an int and its value is 1 list [1 ] is a string and its value is Hellolist [2 ] is a Person and its value is [mike, 18 ]