- 文档
- 教程
【免费下载链接】build-web-application-with-golang
A golang ebook intro how to build a web with golang
本文基于开源电子书 build-web-application-with-golang 第 2.6 章《interface》整理而成。该章节讲解 Go 语言中最精妙的设计之一——interface:从方法签名组合、鸭子类型、空接口,到类型断言、接口嵌入与反射。读完本文,你将掌握如何用 interface 抽象对象行为、编写接受任意类型参数的函数、判断接口变量中的真实类型,并通过反射在运行时检查程序状态,这些能力将直接服务于后续章节中 Web 框架、数据库驱动等模块的设计。
什么是 interface
在 第 2.5 章“面向对象” 中我们学习了method:带接收者的函数,以及匿名字段的 method 继承与重写。interface 正是在此基础上的进一步抽象。
简单的说,interface 是一组 method 签名的组合,我们通过 interface 来定义对象的一组行为。
回顾上一章的最后一个例子:Student和Employee都能SayHi,虽然它们的内部实现不一样(Employee重写了Human的SayHi),但那不重要——重要的是它们都能say hi。
让我们继续做更多的扩展:Student和Employee实现另一个方法Sing,然后Student实现方法BorrowMoney,而Employee实现SpendSalary。
这样Student实现了三个方法:SayHi、Sing、BorrowMoney;而Employee实现了SayHi、Sing、SpendSalary。
上面这些方法的组合称为interface(被对象Student和Employee实现)。例如Student和Employee都实现了由SayHi和Sing组成的 interface,即这两个对象是该 interface 类型。而Employee没有实现由SayHi、Sing和BorrowMoney组成的 interface,因为Employee没有实现BorrowMoney这个方法。
interface 类型:定义与实现规则
interface 类型定义了一组方法,如果某个对象实现了某个接口的所有方法,则此对象就实现了此接口。详细语法参考下面这个例子:
type Human struct { name string age int phone string } type Student struct { Human //匿名字段Human school string loan float32 } type Employee struct { Human //匿名字段Human company string money float32 } //Human对象实现Sayhi方法 func (h *Human) SayHi() { fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone) } // Human对象实现Sing方法 func (h *Human) Sing(lyrics string) { fmt.Println("La la, la la la, la la la la la...", lyrics) } //Human对象实现Guzzle方法 func (h *Human) Guzzle(beerStein string) { fmt.Println("Guzzle Guzzle Guzzle...", beerStein) } // Employee重载Human的Sayhi方法 func (e *Employee) SayHi() { fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name, e.company, e.phone) //此句可以分成多行 } //Student实现BorrowMoney方法 func (s *Student) BorrowMoney(amount float32) { s.loan += amount // (again and again and...) } //Employee实现SpendSalary方法 func (e *Employee) SpendSalary(amount float32) { e.money -= amount // More vodka please!!! Get me through the day! } // 定义interface type Men interface { SayHi() Sing(lyrics string) Guzzle(beerStein string) } type YoungChap interface { SayHi() Sing(song string) BorrowMoney(amount float32) } type ElderlyGent interface { SayHi() Sing(song string) SpendSalary(amount float32) }通过上面的代码我们可以知道:
- interface 可以被任意的对象实现。上面的
Meninterface 被Human、Student和Employee实现——因为Human通过匿名字段机制把SayHi、Sing、Guzzle传递给了内嵌它的Student和Employee,Employee虽然重写了SayHi,但方法集依然完整。 - 一个对象可以实现任意多个 interface。例如上面的
Student实现了Men和YoungChap两个 interface。 - 任意的类型都实现了空 interface(
interface{}),也就是包含 0 个 method 的 interface。
注意这里的接收者是*Human指针形式,这是 第 2.5 章 讲过的“指针作为 receiver”:指针接收者的方法会作用于实例对象本身。这意味着Men接口实际要求的是*Student、*Employee(或*Human)这样的指针类型来满足方法集,这一点在下面的“interface 值”一节会有体现。
interface 值:变量里到底能存什么
如果我们定义了一个 interface 的变量,那么这个变量里面可以存实现这个 interface 的任意类型的对象。例如上面例子中,我们定义了一个Meninterface 类型的变量m,那么m里面可以存Human、Student或者Employee值。
因为m能够持有这三种类型的对象,所以我们可以定义一个包含Men类型元素的 slice——这个 slice 可以被赋予实现了Men接口的任意结构的对象,这和我们传统意义上的 slice 有所不同(传统 slice 的元素类型必须完全一致)。
仓库中 de/code/src/apps/ch.2.6/interface/main.go 保存了这个示例的完整可运行代码,让我们来看一下:
package main import "fmt" type Human struct { name string age int phone string } type Student struct { Human //匿名字段 school string loan float32 } type Employee struct { Human //匿名字段 company string money float32 } //Human实现SayHi方法 func (h Human) SayHi() { fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone) } //Human实现Sing方法 func (h Human) Sing(lyrics string) { fmt.Println("La la la la...", lyrics) } //Employee重载Human的SayHi方法 func (e Employee) SayHi() { fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name, e.company, e.phone) } // Interface Men被Human,Student和Employee实现 // 因为这三个类型都实现了这两个方法 type Men interface { SayHi() Sing(lyrics string) } func main() { mike := Student{Human{"Mike", 25, "222-222-XXX"}, "MIT", 0.00} paul := Student{Human{"Paul", 26, "111-222-XXX"}, "Harvard", 100} sam := Employee{Human{"Sam", 36, "444-222-XXX"}, "Golang Inc.", 1000} tom := Employee{Human{"Tom", 37, "222-444-XXX"}, "Things Ltd.", 5000} //定义Men类型的变量i var i Men //i能存储Student i = mike fmt.Println("This is Mike, a Student:") i.SayHi() i.Sing("November rain") //i也能存储Employee i = tom fmt.Println("This is tom, an Employee:") i.SayHi() i.Sing("Born to be wild") //定义了slice Men fmt.Println("Let's use a slice of Men and see what happens") x := make([]Men, 3) //这三个都是不同类型的元素,但是他们实现了interface同一个接口 x[0], x[1], x[2] = paul, sam, mike for _, value := range x{ value.SayHi() } }运行这个程序,你会看到i先以Student身份调用SayHi,再换成Employee身份调用SayHi,输出内容不同——接口变量中实际保存的“真实对象”决定了调用的具体实现。而x := make([]Men, 3)创建的 slice,三个元素分别是两个Student和一个Employee,却都能通过同一个接口统一遍历调用。
通过上面的代码,你会发现interface 就是一组抽象方法的集合,它必须由其他非 interface 类型实现,而不能自我实现。Go 通过 interface 实现了duck-typing,即:“当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子”。也就是说,只要一个类型实现了接口所需的全部方法,它就被视为该接口类型,无需显式声明implements关键字。
空 interface(interface{})
空 interface(interface{})不包含任何的 method,正因为如此,所有的类型都实现了空 interface。
空 interface 对于描述起不到任何的作用(因为它不包含任何的 method),但是在我们需要存储任意类型的数值的时候相当有用,因为它可以存储任意类型的数值。它有点类似于 C 语言的void*类型。
// 定义a为空接口 var a interface{} var i int = 5 s := "Hello world" // a可以存储任意类型的数值 a = i a = s这里a先后被赋予int和string类型的值,都合法。仓库中的 de/code/src/apps/ch.2.6/reflection/main.go 也演示了空接口的任意存储能力(show_interface_none()函数将字符串、整数、布尔值依次赋给同一个interface{}变量)。
一个函数把interface{}作为参数,那么它可以接受任意类型的值作为参数;如果一个函数返回interface{},那么也就可以返回任意类型的值。
interface 作为函数参数
interface 的变量可以持有任意实现该 interface 类型的对象,这给我们编写函数(包括 method)提供了一些额外的思考:我们是不是可以通过定义 interface 参数,让函数接受各种类型的参数?
举个例子:fmt.Println是我们常用的一个函数,但是你是否注意到它可以接受任意类型的数据。打开 fmt 包的源码文件,你会看到这样一个定义:
type Stringer interface { String() string }也就是说,任何实现了String()string 方法的类型都能作为参数被fmt.Println调用。让我们来试一试:
package main import ( "fmt" "strconv" ) type Human struct { name string age int phone string } // 通过这个方法 Human 实现了 fmt.Stringer func (h Human) String() string { return "❰"+h.name+" - "+strconv.Itoa(h.age)+" years - ✆ " +h.phone+"❱" } func main() { Bob := Human{"Bob", 39, "000-7777-XXX"} fmt.Println("This Human is : ", Bob) }输出时fmt.Println发现Human实现了Stringer接口,就会调用它的String()方法以自定义格式输出,而不是输出默认的结构体字段形式。
现在我们再回顾一下 第 2.5 章 的 Box 示例,你会发现Color结构也定义了一个 method:String。其实这也是实现了fmt.Stringer这个 interface。也就是说,如果需要某个类型能被 fmt 包以特殊的格式输出,你就必须实现Stringer这个接口;如果没有实现这个接口,fmt 将以默认的方式输出。
//实现同样的功能 fmt.Println("The biggest one is", boxes.BiggestsColor().String()) fmt.Println("The biggest one is", boxes.BiggestsColor())上面两行代码输出完全一样:第一行是显式调用String(),第二行是 fmt 内部发现实现了Stringer自动调用。
注:实现了error接口的对象(即实现了Error() string的对象),使用 fmt 输出时,会调用Error()方法,因此不必再定义String()方法了。
判断 interface 变量中存储的类型
我们知道 interface 的变量里面可以存储任意类型的数值(该类型实现了 interface)。那么我们怎么反向知道这个变量里面实际保存的是哪个类型的对象呢?目前常用的有两种方法:Comma-ok 断言和switch 测试。
Comma-ok 断言
Go 语言里面有一个语法,可以直接判断是否是该类型的变量:value, ok = element.(T)。这里value就是变量的值,ok是一个 bool 类型,element是 interface 变量,T是断言的类型。
如果element里面确实存储了T类型的数值,那么ok返回true,否则返回false。
仓库中的 de/code/src/apps/ch.2.6/type_check/main.go 保存了这个例子的完整可运行代码:
package main import ( "fmt" "strconv" ) type Element interface{} type List [] Element type Person struct { name string age int } //定义了String方法,实现了fmt.Stringer func (p Person) String() string { return "(name: " + p.name + " - age: "+strconv.Itoa(p.age)+ " years)" } func main() { list := make(List, 3) list[0] = 1 // an int list[1] = "Hello" // a string list[2] = Person{"Dennis", 70} 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\n", index, value) } else { fmt.Printf("list[%d] is of a different type\n", index) } } }这里List是[]Element,而Element是空接口,所以这个 slice 可以同时容纳int、string和Person三种不同类型的元素。遍历时通过element.(int)、element.(string)、element.(Person)逐一分流处理。
是不是很简单啊?同时你是否注意到多个if里面,还记得前面讲流程时讲过,if 里面允许初始化变量(value, ok :=就是在 if 条件中完成声明与赋值的)。
也许你注意到了,我们断言的类型越多,那么if else也就越多,所以才引出了下面要介绍的 switch。
switch 测试
最好的讲解就是代码例子,现在让我们重写上面的这个实现。仓库中的 de/code/src/apps/ch.2.6/switch_type_check/main.go 保存了完整可运行代码:
package main import ( "fmt" "strconv" ) type Element interface{} type List [] Element type Person struct { name string age int } //打印 func (p Person) String() string { return "(name: " + p.name + " - age: "+strconv.Itoa(p.age)+ " years)" } func main() { list := make(List, 3) list[0] = 1 //an int list[1] = "Hello" //a string list[2] = Person{"Dennis", 70} 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\n", index, value) default: fmt.Println("list[%d] is of a different type", index) } } }这里有一点需要强调:element.(type)语法不能在 switch 外的任何逻辑里面使用。如果你要在 switch 外面判断一个类型,就使用comma-ok。
嵌入 interface
Go 里面真正吸引人的是它内置的逻辑语法,就像我们在学习 struct 时学习的匿名字段一样优雅。那么把相同的逻辑引入到 interface 里面,不是更加完美吗:如果一个 interface1 作为 interface2 的一个嵌入字段,那么 interface2 隐式的包含了 interface1 里面的 method。
我们可以看到源码包container/heap里面有这样的一个定义:
type Interface interface { sort.Interface //嵌入字段sort.Interface Push(x interface{}) //a Push method to push elements into the heap Pop() interface{} //a Pop elements that pops elements from the heap }我们看到sort.Interface其实就是嵌入字段,把sort.Interface的所有 method 给隐式的包含进来了,也就是下面三个方法:
type Interface interface { // Len is the number of elements in the collection. Len() int // Less returns whether the element with index i should sort // before the element with index j. Less(i, j int) bool // Swap swaps the elements with indexes i and j. Swap(i, j int) }也就是说,任何类型想要使用container/heap,就必须同时实现Len、Less、Swap、Push、Pop这五个方法——其中前三个来自嵌入的sort.Interface,后两个是 heap 自己声明的。这正是 Go 标准库中“组合优于继承”的典型体现。
另一个例子就是 io 包下面的io.ReadWriter,它包含了 io 包下面的Reader和Writer两个 interface:
// io.ReadWriter type ReadWriter interface { Reader Writer }任何实现了Read与Write两种方法的类型,都自动满足io.ReadWriter,无需显式声明。这种“小接口组合成大接口”的设计模式在标准库中随处可见。
反射
Go 语言实现了反射。所谓反射,就是能检查程序在运行时的状态。我们一般用到的包是reflect包。Go 官方有一篇著名的文章《The Laws of Reflection》详细讲解了 reflect 包的实现原理,这里不再给出外部链接,直接结合代码讲解用法。
使用 reflect 一般分成三步,下面简要的讲解一下:要去反射一个类型的值(这些值都实现了空 interface),首先需要把它转化成 reflect 对象(reflect.Type或者reflect.Value,根据不同的情况调用不同的函数)。这两种获取方式如下:
t := reflect.TypeOf(i) //得到类型的元数据,通过t我们能获取类型定义里面的所有元素 v := reflect.ValueOf(i) //得到实际的值,通过v我们获取存储在里面的值,还可以去改变值转化为 reflect 对象之后我们就可以进行一些操作了,也就是将 reflect 对象转化成相应的值,例如:
tag := t.Elem().Field(0).Tag //获取定义在struct里面的标签 name := v.Elem().Field(0).String() //获取存储在第一个字段里面的值注意这里的Elem():当传入的是指针时,需要通过Elem()解引用才能拿到指针指向的结构体本身,从而访问其字段和标签。获取反射值能返回相应的类型和数值:
var x float64 = 3.4 v := reflect.ValueOf(x) fmt.Println("type:", v.Type()) fmt.Println("kind is float64:", v.Kind() == reflect.Float64) fmt.Println("value:", v.Float())仓库中的 de/code/src/apps/ch.2.6/reflection/main.go 将空接口存储与反射的两个示例整合为可运行程序:show_interface_none()演示interface{}任意存储,show_reflection()演示类型、Kind 的获取以及通过指针修改值。
最后,反射的字段必须是可修改的。我们前面学习过传值和传引用,这个里面也是一样的道理。反射的字段必须是可读写的意思是,如果下面这样写,那么会发生错误(运行时 panic:reflect.Value.SetFloat using unaddressable value):
var x float64 = 3.4 v := reflect.ValueOf(x) v.SetFloat(7.1)因为v是从值拷贝得到的reflect.Value,它指向的并不是原始变量x本身,Go 不允许通过它修改不可寻址的值。如果要修改相应的值,必须这样写——先取指针,再Elem()解引用获得可写的 Value:
var x float64 = 3.4 p := reflect.ValueOf(&x) v := p.Elem() v.SetFloat(7.1)反射虽然强大,但也伴随着运行时的类型检查开销和代码可读性下降。在实际项目中,能通过 interface 和类型断言解决的问题,应优先使用静态类型方案;反射更适合在编写通用库、序列化框架、ORM 等确实需要动态处理任意类型的场景中使用。上面只是对反射的简单介绍,更深入的理解还需要自己在编程中不断的实践。
总结
interface 是 Go 语言面向对象设计的核心:
- 行为抽象:interface 是一组方法签名的集合,对象实现全部方法即自动满足接口,无需显式声明(鸭子类型);
- 多态容器:接口变量可以持有任意实现该接口的类型,接口 slice 可以混装不同具体类型的元素;
- 万能参数:空接口
interface{}可存储任意类型,配合fmt.Stringer、error等内置接口让标准库函数拥有极高的通用性; - 运行时类型判断:通过 Comma-ok 断言和
switch element.(type)反向获取接口变量中的真实类型; - 组合扩展:通过嵌入接口(如
container/heap的Interface、io.ReadWriter)实现小接口组合成大接口; - 反射:
reflect.TypeOf/reflect.ValueOf在运行时检查类型与值,配合指针Elem()实现可修改的反射写入。
这些能力将在本电子书后续章节中反复出现:例如 第 5.1 章 database/sql 接口 中Driver、Conn、Stmt、Rows等大量接口的定义,正是 interface 驱动数据库驱动插件化设计的实际应用;第 13 章 Web 框架设计 中的ControllerInterface等框架级接口同样依赖本文的机制。建议在阅读后续内容前,动手运行一遍仓库中de/code/src/apps/ch.2.6/目录下的四个示例程序,把本文的每一个概念落到可执行的代码上。
相关阅读
- 目录
- 上一章:面向对象(method、匿名字段继承与重写)
- 下一节:并发
- 文档
- 教程
【免费下载链接】build-web-application-with-golang
A golang ebook intro how to build a web with golang
相关推荐
Go 语言接口(interface)完全指南:从鸭子类型到反射 —— 基于《Build Web Application with Golang》2.6 节
Go 语言接口(interface)完全指南:从鸭子类型到反射 —— 基于《Build Web Application with Golang》2.6 节 接口
文档教程深入理解 Go 接口(Interface)与反射:从方法集合到运行时类型探测
深入理解 Go 接口(Interface)与反射:从方法集合到运行时类型探测 本文是《Go Web 编程》(build web application with
文档教程GordenPPTSkill 上手实测:AI时代的PPT制作,原来可以只改文字不动排版
GordenPPTSkill 上手实测:AI时代的PPT制作,原来可以只改文字不动排版 GordenPPTSkill 是什么?一句话:一个面向 AI 助手的 P
人工智能AI 技能
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考