ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

深入理解 Go 语言 interface:鸭子类型、类型断言与反射

深入理解 Go 语言 interface:鸭子类型、类型断言与反射 文档教程【免费下载链接】build-web-application-with-golangA golang ebook intro how to build a web with golang项目地址https://gitcode.com/gh_mirrors/bu/build-web-application-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和EmployeeEmployee虽然重写了SayHi但方法集依然完整。一个对象可以实现任意多个 interface。例如上面的Student实现了Men和YoungChap两个 interface。任意的类型都实现了空 interfaceinterface{}也就是包含 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(Lets 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关键字。空 interfaceinterface{}空 interfaceinterface{}不包含任何的 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结构也定义了一个 methodString。其实这也是实现了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。嵌入 interfaceGo 里面真正吸引人的是它内置的逻辑语法就像我们在学习 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 的获取以及通过指针修改值。最后反射的字段必须是可修改的。我们前面学习过传值和传引用这个里面也是一样的道理。反射的字段必须是可读写的意思是如果下面这样写那么会发生错误运行时 panicreflect.Value.SetFloat using unaddressable valuevar x float64 3.4 v : reflect.ValueOf(x) v.SetFloat(7.1)因为v是从值拷贝得到的reflect.Value它指向的并不是原始变量x本身Go 不允许通过它修改不可寻址的值。如果要修改相应的值必须这样写——先取指针再Elem()解引用获得可写的 Valuevar 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-golangA golang ebook intro how to build a web with golang项目地址https://gitcode.com/gh_mirrors/bu/build-web-application-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 技能上一篇从混乱到秩序用QRemeshify重塑你的3D建模工作流下一篇WeWe RSS 私有化部署实操把微信公众号变成自己的 RSS 信息流创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表