Go语言开发(五)、Go语言面向接口
一、Duck Typing简介
1、Duck Typing简介
对于一门强类型的静态语言来说,要想通过运行时多态来隔离变化,多个实现类就必须属于同一类型体系,必须通过继承的方式与同一抽象类型建立is-a关系。
而Duck Typing则是一种基于特征,而不是基于类型的多态方式。Duck Typing仍然关心is-a,只不过is-a关系是以对方是否具备相关的特征来确定的。
是否满足is-a关系可以使用所谓的鸭子测试(Duck Test)进行判断。
"当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。"
Go语言开发(五)、Go语言面向接口
Duck Test是基于特征的哲学,给设计提供了强大的灵活性。动态面向对象语言,如Python,Ruby等都遵从了Duck Test来实现运行时多态。
2、C++对Duck Typing的支持
Duck Typing并不是动态语言的专利。C++作为一门强类型的静态语言,也对Duck Typing特性有强有力的支持。不过C++对Duck Typing特性支持不是在运行时,而是在编译时。
C++通过泛型编程实现对Duck Typing的支持。对于一个模板类或模板函数,会要求其实例化的类型必须具备某种特征,如某个函数签名、某个类型定义、某个成员变量等等。如果特征不具备,编译器会报错。
因此C++模板类、模板函数对要实例化的客户类提出了特征要求,客户类型需要实现相应的特征要求,从而复用模板的实现。
Duck Typing需要实例化的类型具备一致的特征,而模板特化的作用正是为了让不同类型具有统一的特征(统一的操作界面),所以模板特化可以作为Duck Typing与实例化类型之间的适配器。这种模板特化手段称为萃取(Traits),其中类型萃取最为常见。
类型萃取首先是一种非侵入性的中间层。否则,这些特征就必须被实例化类型提供,而就意味着,当一个实例化类型需要复用多个Duck Typing模板时,就需要迎合多种特征,从而让自己经常被修改,并逐渐变得庞大和难以理解。
Go语言开发(五)、Go语言面向接口
一个Duck Typing模板,比如一个通用算法,需要实例化类型提供一些特征时,如果一个类型是类,则是一件很容易的事情,因为你可以在一个类里定义任何需要的特征。但如果一个基本类型也想复用此通用算法,由于基本类型无法靠自己提供算法所需要的特征,就必须借助于类型萃取。
3、Go语言对Duck Typing的支持
Go语言作为一种静态语言,对Duck Typing的支持通过Structural Typing实现。
Structural Typing是Go语言式的接口,就是不用显示声明类型T实现了接口I,只要类型T的公开方法完全满足接口I的要求,就可以把类型T的对象用在需要接口I的地方。
package main
import "fmt"
type ISayHello interface {
sayHello()
}
//美国人
type AmericalPerson struct {}
func (person AmericalPerson)sayHello(){
fmt.Println("Hello!")
}
//中国人
type ChinesePerson struct {}
func (person ChinesePerson)sayHello(){
fmt.Println("你好!")
}
func greet(i ISayHello){
i.sayHello()
}
func main() {
ameriacal := AmericalPerson{}
chinese := ChinesePerson{}
var i ISayHello
i = ameriacal
i.sayHello()
i = chinese
i.sayHello()
}
二、接口的定义和实现
1、接口的定义
Go语言的接口是一种抽象数据类型,是一系列接口的集合,接口把所有的具有共性的方法定义在一起,任何其它类型只要实现了接口定义的方法就是实现了接口。接口是duck-type编程的一种体现,不关心属性(数据),只关心行为(方法)。
Go语言的接口由使用者定义。
接口的声明语法如下:
/* 定义接口 */
type interface_name interface {
method_name1 [return_type]
method_name2 [return_type]
method_name3 [return_type]
...
method_namen [return_type]
}
面向对象编程思想中事物的共同点构成了抽象的基础,继承关系解决了IS-A即定义问题,因此可以把子类对象当做父类对象使用。但对于父类不同但又具有某些共同行为的数据,继承不能解决。单一继承构造的是树状结构,而现实世界中常见的是网状结构。
接口是在某一个方面的抽象,但不同于继承,接口是松散的结构,不与定义绑定。Duck Typing相比继承是更加松耦合的方式,可以同时从多个维度对数据进行抽象,找出共同点,并使用同一套逻辑来处理。
面向对象语言如Java、C++的接口方式是先声明后实现的强制模式,Go语言则不需要声明接口,
实现之间应该少用继承式的强关联关系,多用接口式的弱关联关系。接口已经可以在很多方面替代继承的作用,比如多态和泛型,而且接口的关系松散、随意,可以有更高的自由度、更多的抽象角度。
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
type Seeker interface {
Seek(offset int64, whence int) (int64, error)
}
2、接口的实现
接口的实现是隐式的,不需要显示声明实现了接口,只需要实现接口的所有方法。接口的实现语法如下:
/* 定义结构体 */
type struct_name struct {
/* variables */
}
/* 实现接口方法 */
func (struct_name_variable struct_name) method_name1() [return_type] {
/* 方法实现 */
}
...
func (struct_name_variable struct_name) method_namen() [return_type] {
/* 方法实现*/
}
假设在另一个地方中定义File类型:
type File struct { // ...
}
func (f *File) Read(buf []byte) (n int, err error)
func (f *File) Write(buf []byte) (n int, err error)
func (f *File) Seek(off int64, whence int) (pos int64, err error)
func (f *File) Close() error
File实现了4个接口,因此可以将File对象赋值给任何一个接口。
var file1 Reader = new(File)
var file2 Writer = new(File)
var file3 Closer = new(File)
var file4 Seeker = new(File)
3、接口的定义与实现实例
package main
import "fmt"
//接口的定义
type ISayHello interface {
sayHello()
}
//接口的实现
//美国人
type AmericalPerson struct {}
func (person AmericalPerson)sayHello(){
fmt.Println("Hello!")
}
//接口的实现
//中国人
type ChinesePerson struct {}
func (person ChinesePerson)sayHello(){
fmt.Println("你好!")
}
func greet(i ISayHello){
i.sayHello()
}
func main() {
ameriacal := AmericalPerson{}
chinese := ChinesePerson{}
var i ISayHello
i = ameriacal
i.sayHello()
i = chinese
i.sayHello()
}
三、接口的值类型
1、接口的值类型
一个类型可以实现任意数量的接口,每个类型都实现了一个空接口interface{}。
接口是一系列接口的集合,是一种抽象数据类型,接口变量可以引用任何实现了接口的全部方法的具体数据类型的值。
接口变量存储了两部分信息,一个是分配给接口变量的具体值(接口实现者的值),一个是值的类型的描述器(接口实现者的类型),形式是(value, concrete type),而不是(value, interface type)。
实现接口的具体方法时,如果以指针作为接收者,接口的具体实现类型只能以指针方式使用,值接收者既可以按指针方式使用也可以按值方式使用。
package main
import "fmt"
type Retriever interface {
Get(url string) string
}
type MockRetriever struct {
Contents string
}
// 值接收者
func (r MockRetriever) Get(url string) string {
return r.Contents
}
type RealRetriever struct {
Contents string
}
// 指针接收者
func (r *RealRetriever) Get(url string) string {
return r.Contents
}
func main() {
var retriever Retriever
retriever = MockRetriever{"This is fake Retreiver"}
fmt.Printf("%T %v\n", retriever, retriever)
retriever = &MockRetriever{"This is fake Retreiver"}
fmt.Printf("%T %v\n", retriever, retriever)
retriever = &RealRetriever{"This is real Retriever"}
//retriever = RealRetriever{"This is real Retriever"} //error
fmt.Printf("%T %v\n", retriever, retriever)
}
// output:
// main.MockRetriever {This is fake Retreiver}
// *main.MockRetriever &{This is fake Retreiver}
// *main.RealRetriever &{This is real Retriever}
2、空接口
空接口类型interface{}一个方法签名也不包含,所以所有的数据类型都实现了空接口。
空接口类型可以用于存储任意数据类型的实例。
如果一个函数的参数是空接口类型interface{},表明可以使用任何类型的数据。如果一个函数返回一个空接口类型,表明函数可以返回任何类型的数据。
interface{}可用于向函数传递任意类型的变量,但对于函数内部,该变量仍然为interface{}类型(空接口类型),而不是传入的实参类型。
利用接口类型作为参数可以达到抽象数据类型的目的。
定义一个MaxInterface接口,包含三个方法签名:
Len() int:必须返回集合数据结构的长度
Get(int i) interface{}:必须返回一个在索引i的数据元素
Bigger(i, j int) bool: 返回位于索引i和j的数值比较结果
满足MaxInterface接口的数据类型需要实现以上三个方法。
package main
import "fmt"
//Person类型
type Person struct{
name string
age int
}
//切片类型
type IntSlice []int
type FloatSlice []float32
type PersonSlice []Person
//接口定义
type MaxInterface interface {
Len() int
Get(i int)interface{}
Bigger(i,j int)bool
}
//Len()方法的实现
func (x IntSlice) Len()int{
return len(x)
}
func (x FloatSlice) Len()int{
return len(x)
}
func (x PersonSlice) Len()int{
return len(x)
}
//Get(i int)方法实现
func (x IntSlice) Get(i int)interface{}{
return x[i]
}
func (x FloatSlice) Get(i int)interface{}{
return x[i]
}
func (x PersonSlice) Get(i int)interface{}{
return x[i]
}
//Bigger(i,j int)方法实现
func (x IntSlice) Bigger(i,j int)bool{
if x[i] > x[j]{
return true
}else{
return false
}
}
func (x FloatSlice) Bigger(i,j int)bool{
if x[i] > x[j]{
return true
}else {
return false
}
}
func (x PersonSlice) Bigger(i,j int)bool{
if x[i].age > x[j].age{
return true
}else {
return false
}
}
//求最大值函数实现
func Max(data MaxInterface) (ok bool, max interface{}){
if data.Len() == 0{
return false,nil
}
if data.Len() == 1{
return true,data.Get(1)
}
max = data.Get(0)
m := 0
for i:=1;i<data.Len();i++{
if data.Bigger(i,m){
max = data.Get(i)
m = i
}
}
return true, max
}
func main() {
intslice := IntSlice{1, 2, 44, 6, 44, 222}
floatslice := FloatSlice{1.99, 3.14, 24.8}
group := PersonSlice{
Person{name:"Jack", age:24},
Person{name:"Bob", age:23},
Person{name:"Bauer", age:104},
Person{name:"Paul", age:44},
Person{name:"Sam", age:34},
Person{name:"Lice", age:54},
Person{name:"Karl", age:74},
Person{name:"Lee", age:4},
}
_,m := Max(intslice)
fmt.Println("The biggest integer in islice is :", m)
_, m = Max(floatslice)
fmt.Println("The biggest float in fslice is :", m)
_, m = Max(group)
fmt.Println("The oldest person in the group is:", m)
}
[]T不能直接赋值给[]interface{}
t := []int{1, 2, 3, 4}
var s []interface{} = t
编译时报错:
cannot use t (type []int) as type []interface {} in assignment
正确赋值方法:
t := []int{1, 2, 3, 4}
s := make([]interface{}, len(t))
for i, v := range t {
s[i] = v
}
3、类型断言
interface{}可用于向函数传递任意类型的变量,但对于函数内部,该变量仍然为interface{}类型(空接口类型),而不是传入的实参类型。
接口类型向普通类型的转换称为类型断言(运行期确定)。
func printArray(arr interface{}){
//arr是空接口,不是数组类型,报错
for _,v:=range arr{
fmt.Print(v," ")
}
fmt.Println()
}
可以通过类型断言将接口类型转换为切片类型。
func printArray(arr interface{}){
//通过断言实现类型转换
a,_ := arr.([]int)
for _,v:=range a{
fmt.Println(v, " ")
}
fmt.Println()
}
在使用类型断言时,最好判断断言是否成功。
b,ok := a.(T)
if ok{
...
}
断言失败在编译阶段不会报错,因此,如果不对断言结果进行判断将可能会断言失败导致运行错误。
不同类型变量的运算必须进行显式的类型转换,否者结果可能会溢出,导致出错。
类型断言也可以配合switch语句进行判断。
var t interface{}
t = functionOfSomeType()
switch t := t.(type) {
default:
fmt.Printf("unexpected type %T", t) // %T prints whatever type t has
case bool:
fmt.Printf("boolean %t\n", t) // t has type bool
case int:
fmt.Printf("integer %d\n", t) // t has type int
case *bool:
fmt.Printf("pointer to boolean %t\n", *t) // t has type *bool
case *int:
fmt.Printf("pointer to integer %d\n", *t) // t has type *int
}
四、接口赋值
可以将一个实现接口的对象实例赋值给接口,也可以将另外一个接口赋值给接口。
1、通过对象实例赋值
将一个类对象实例赋值给一个接口前,要保证类实现了接口的所有方法。
package main
import (
"fmt"
"reflect"
)
// 定义接口
type Animal interface {
eat()
drink()
print()
}
type Bird struct {
Name string
}
func (b *Bird) eat() {
fmt.Println(b.Name,"eat")
}
func (b *Bird) drink() {
fmt.Println(b.Name,"drink")
}
func (b *Bird) print() {
fmt.Println(b.Name,"is Bird")
}
type People struct {
Name string
}
func (p People) eat() {
fmt.Println(p.Name,"eat")
}
func (p People) drink() {
fmt.Println(p.Name,"drink")
}
func (p People) print() {
fmt.Println(p.Name, "is People.")
}
func main() {
var bob People = People{"BoB"}
var people1 Animal = &bob
people1.print() // BoB is People.
fmt.Println(reflect.TypeOf(people1)) // *main.People
var people2 Animal = bob
people2.print() // BoB is People.
fmt.Println(reflect.TypeOf(people2)) // main.People
var pigeon Bird = Bird{"Jack."}
var bird1 Animal = &pigeon
bird1.print() // Jack is Bird.
fmt.Println(reflect.TypeOf(bird1)) // *main.Bird
//var bird2 Animal = pigeon // not ok
//cannot use pigeon (type Bird) as type Animal in assignment:
//Bird does not implement Animal (drink method has pointer receiver)
}
2、通过接口赋值
var r io.Reader = new(os.File)
var rw io.ReadWriter = r //not ok
var rw2 io.ReadWriter = new(os.File)
var r2 io.Reader = rw2 //ok
r没有实现Write方法,所以不能赋值给rw。
五、反射机制
Go语言实现了反射,所谓反射就是能检查程序在运行时的状态。
reflect包实现了运行时反射,允许程序操作任意类型的对象。
1、获取Value、Type对象
将变量转化成reflect对象(reflect.Type或者reflect.Value)
t := reflect.TypeOf(i) //reflect.Type对象
v := reflect.ValueOf(i) //reflect.Value对象
调用reflect.TypeOf(x),x首先存储在一个空接口上,然后在作为参数传递给TypeOf函数; Reflect.TypeOf函数内部会解析空接口,接收类型信息。
同理,reflect.ValueOf函数内部会接收到一个value信息。
2、获取对象或者变量的类型
Value.Type()和Value.Kind()方法都可以获取对象或者变量的类型,如果是变量的话,获取到的类型都相同;如果是结构体对象,Value.Type()返回结构体的名称,Value.Kind()返回“struct”;如果是自定义类型,Value.Type()返回自定义类型名称,Value.Kind()返回自定义类型的底层存储类型。因此,Value.Kind()可以用于判断变量是否是结构体。
Kind()描述的是reflection对象的底层类型,而不是静态类型。假如一个reflection对像包含了一个用户自定义的静态类型,Kind()方法返回的是底层数据类型,而不是自定义静态类型。
package main
import (
"reflect"
"fmt"
)
type Float float64
type Person struct {
name string
age int
}
func main() {
var x1 int = 8
value1 := reflect.ValueOf(x1)
fmt.Println(value1.Type())//int
fmt.Println(value1.Kind())//int
var x Float = 3.14
value2 := reflect.ValueOf(x)
fmt.Println(value2.Type())//Float
fmt.Println(value2.Kind())//float64
person := Person{}
value3 := reflect.ValueOf(person)
fmt.Println(value3.Type())//Person
fmt.Println(value3.Kind())//struct
}
3、获取变量的值和给变量赋值
获取变量的值使用value.Interface()方法,返回一个value的值,类型是interface。给变量赋值需要先判断变量的类型,可以使用Value.Kind()方法,如果变量的类型是reflect.Int,使用Value.SetInt()方法给变量赋值。
如果要修改reflection对象的值,reflection对象的值必须是可settable的。
Settability(可设置)是reflection Value的一个属性, 并不是所有的reflection Values都拥有Settability属性。Settability属性表示reflection对象是否可以修改创建reflection对象的实际值,可设置取决于reflection对象所持有的原始值。
调用reflect.ValueOf(x)时,x作为参数传递时,首先拷贝x,reflect.ValueOf函数中的interface值是x的拷贝,而不是x本身。如果想要通过reflection修改x, 必须传递一个x指针,获取reflect.Value指针指向的对象,使用reflect.ValueOf(&x).Elem()。
package main
import (
"reflect"
"fmt"
)
type Float float64
type Human struct {
name string
Age uint8
}
func main() {
var x1 int = 8
value1 := reflect.ValueOf(x1)
fmt.Println(value1.Type())//int
fmt.Println(value1.Kind())//int
var x Float = 3.14
//获取reflect.Value对象,属性Settability为false
value2 := reflect.ValueOf(x)
fmt.Println(value2.Type())//Float
fmt.Println(value2.Kind())//float64
if value2.Kind() ==reflect.Float64{
if value2.CanSet(){
value2.SetFloat(3.1415926)
}
}
fmt.Println(value2)//3.14
person := Human{"Bauer",30}
//获取reflect.Value指针指向的对象,属性Settability为true
value3 := reflect.ValueOf(&person).Elem()
fmt.Println(value3.Type())//Person
fmt.Println(value3.Kind())//struct
fmt.Println(value3)//{Bauer 30}
field0 := value3.FieldByName("name")
if field0.Kind() == reflect.String{
if field0.CanSet(){//私有成员不可设置
field0.SetString("Bob")
}
}
fmt.Println(value3)//{Bauer 30}
field1 := value3.FieldByName("Age")
if field1.Kind() == reflect.Uint8{
if field1.CanSet(){//公有成员可设置
field1.SetUint(20)
}
}
fmt.Println(value3)//{Bauer 20}
}
对于结构体,只有公有的成员变量可以被reflect改变值,私有的变量是无法改变值的。
4、获取结构体成员变量的tag信息
由于golang变量大小写和公有私有权限相关,开发者很难按照自己的意愿来定义变量名,因此golang提供了tag机制,用于给变量提供一个标签,标签可以作为一个别名,来给一些存储结构来获取结构体变量名字使用。
type Person struct {
name string `Country:"CN"`
age uint8
}
bob := Person{"Bob", 30}
v := reflect.ValueOf(bob)
vt := v.Type()
filed,_ := vt.FieldByName("name")
fmt.Println(filed.Tag.Get("Country"))//CN
六、接口的组合
1、结构体嵌入类型
结构体类型可以包含匿名或者嵌入字段。当嵌入一个类型到结构体中时,嵌入类型的名字充当了嵌入字段的字段名。
package main
import "fmt"
type User struct {
Name string
EMail string
}
type Admin struct {
User
Level string
}
func (user *User)Notify() error{
fmt.Printf("User: Sending a Email to %s<%s>\n", user.Name,user.EMail)
return nil
}
func main() {
admin := &Admin{
User: User{
Name: "Bauer",
EMail: "bauer@gmail.com",
},
Level: "super",
}
admin.Notify()
admin.User.Notify()
}
当嵌入一个类型,嵌入类型的方法就变成了外部类型的方法,但是当嵌入类型的方法被调用时,方法的接受者是内部类型(嵌入类型),而非外部类型。
嵌入类型的名字充当着字段名,同时嵌入类型作为内部类型存在,可以使用以下方式的调用方法:
admin.User.Notify()
上述代码通过类型名称来访问内部类型的字段和方法。内部类型的字段和方法也同样被提升到了外部类型,因此可以使用以下方式调用方法:
admin.Notify()
通过外部类型来调用Notify方法,本质上是内部类型的方法。
2、Go语言的方法提升
Go语言中内部类型方法集提升的规则如下:
A、如果S包含一个匿名字段T,S和S的方法集都包含接收者为T的方法提升。
当嵌入一个类型,嵌入类型的接收者为值类型的方法将被提升,可以被外部类型的值和指针调用。
B、对于S类型的方法集包含接收者为T的方法提升
当嵌入一个类型,可以被外部类型的指针调用的方法集只有嵌入类型的接收者为指针类型的方法集,即当外部类型使用指针调用内部类型的方法时,只有接收者为指针类型的内部类型方法集将被提升。
C、如果S包含一个匿名字段T,S和S的方法集都包含接收者为T或者T 的方法提升
当嵌入一个类型的指针,嵌入类型的接收者为值类型或指针类型的方法将被提升,可以被外部类型的值或者指针调用。
D、如果S包含一个匿名字段T,S的方法集不包含接收者为*T的方法提升。
根据Go语言规范里方法提升中的三条规则推导出的规则。当嵌入一个类型,嵌入类型的接收者为指针的方法将不能被外部类型的值访问。
3、接口的组合
GO语言中可以通过接口的组合,创建新的接口,新的接口默认继承组合的接口的抽象方法。
package main
import "fmt"
type IReader interface {
Read(file string) []byte
}
type IWriter interface {
Write(file string, data string)
}
// 接口组合,默认继承了IReader和IWriter中的抽象方法
type IReadWriter interface {
IReader
IWriter
}
type ReadWriter struct {
}
func (rw *ReadWriter) Read(file string) []byte {
fmt.Println(file)
return nil
}
func (rw *ReadWriter) Write(file string, data string) {
fmt.Printf("filename:%s, contents:%s",file,data)
}
func main() {
readwriter := new(ReadWriter)
var irw IReadWriter = readwriter // ok
irw.Read("abc.txt")
data := "hello world."
irw.Write("abc.txt",data)
}
有疑问加站长微信联系(非本文作者))