ARTICLE DETAIL

资讯详情

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

深入理解 Go 语言接口(Interfaces):从方法集、空接口到反射——《build-web-application-with-golang》第 2.6 节实战解读

深入理解 Go 语言接口(Interfaces):从方法集、空接口到反射——《build-web-application-with-golang》第 2.6 节实战解读 文档教程【免费下载链接】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点击查看免费下载接口interface是 Go 语言中最精妙的设计特性之一它通过方法集实现了鸭子类型duck typing让任何满足方法签名的类型都能无缝参与多态。本文以《build-web-application-with-golang》第 2.6 节为骨架结合仓库中 ch.2.6 源码示例 逐层拆解接口的定义与实现规则、接口值的内存语义、空接口与fmt.Stringer、comma-ok 与 type switch 类型断言、接口嵌入以及基于reflect的反射机制。读完你将能熟练设计面向接口的程序并正确使用类型断言与反射完成运行时类型探测与修改。什么是接口简单来说接口是一组方法的集合用来定义一组行为actions。它不关心你是谁只关心你能做什么。回顾前几节面向对象的例子第 2.5 节Student和Employee都可以调用SayHi()但它们做的是不同的事——学生打招呼、员工打招呼并报出公司名。在此基础上我们继续扩展给两者都加上Sing()方法给Student加上BorrowMoney()给Employee加上SpendSalary()。此时Student拥有SayHi()、Sing()、BorrowMoney()三个方法Employee拥有SayHi()、Sing()、SpendSalary()三个方法。这种方法的组合就称为接口Student和Employee都实现了接口{SayHi(), Sing()}Employee没有实现{SayHi(), Sing(), BorrowMoney()}因为它没有BorrowMoney()Student没有实现{SayHi(), Sing(), SpendSalary()}因为它没有SpendSalary()。这正是 Go 接口的精髓接口的实现是隐式的implicit不需要像 Java 那样显式声明implements只要类型的方法集包含接口的全部方法该类型就自动实现了这个接口。接口类型定义与实现规则一个接口定义一组方法如果一个类型实现了接口中的所有方法我们就说该类型实现了这个接口。以下代码定义了一个Human基结构体、两个子结构体以及三种接口对应仓库中的 interface/main.go 示例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 } func (h *Human) SayHi() { fmt.Printf(Hi, I am %s you can call me on %s\n, h.name, h.phone) } func (h *Human) Sing(lyrics string) { fmt.Println(La la, la la la, la la la la la..., lyrics) } func (h *Human) Guzzle(beerStein string) { fmt.Println(Guzzle Guzzle Guzzle..., beerStein) } // Employee 重写了 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) } func (s *Student) BorrowMoney(amount float32) { s.loan amount } func (e *Employee) SpendSalary(amount float32) { e.money - amount } // 定义一些接口 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) }从这段代码可以提炼出三条关键规则接口可以由任何类型实现无论是自定义结构体还是内置类型只要方法集匹配即可一个类型可以同时实现多个接口例如Student同时满足Men少一个Guzzle时不满足和YoungChap所有类型都实现空接口interface{}空接口不包含任何方法而任何类型天生拥有零个方法因此所有类型都隐式实现了interface{}。注意第 55 行Employee通过方法重写覆盖了从Human继承的SayHi()这与 第 2.5 节 讲解的方法重载overload与嵌入式结构体方法提升一脉相承。接口的值Interface Value接口变量能存放什么样的值只要定义了某接口类型的变量任何实现了该接口的类型都可以赋给它。例如定义var i Men则Student、Human、Employee的实例都能赋给i。由此我们可以构造一个Men的切片slice把任意实现Men的类型放进去——这就是 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 } func (h Human) SayHi() { fmt.Printf(Hi, I am %s you can call me on %s\n, h.name, h.phone) } func (h Human) Sing(lyrics string) { fmt.Println(La la la la..., lyrics) } 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) } // 接口 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{Sam, 36, 444-222-XXX}, Things Ltd., 5000} // 定义接口变量 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) // Men 类型的切片 fmt.Println(Lets use a slice of Men and see what happens) x : make([]Men, 3) // 这三个元素类型各不相同但都实现了接口 Men x[0], x[1], x[2] paul, sam, mike for _, value : range x { value.SayHi() } }运行后可以看到i.SayHi()在保存Student时调用学生的打招呼逻辑在保存Employee时调用员工的打招呼逻辑——这就是运行时多态。仓库中的可运行版本见 ch.2.6/interface/main.go。需要特别注意接口是抽象方法的集合只能由非接口类型实现接口本身不能实现自己例如不能用一个接口去实现另一个接口的方法集——嵌入接口是另一回事见下文。此外接口切片[]Men与具体类型切片[]Student的行为并不相同前者每个元素都携带动态类型 动态值信息后者是连续的内存布局。空接口Empty Interface空接口是不包含任何方法的接口因此所有类型都实现了它。当我们需要在同一个容器里存放任意类型的值时空接口非常有用功能上类似于 C 语言中的void*// 定义空接口 var a interface{} var i int 5 s : Hello world // a 可以存放任何类型的值 a i a s函数签名中的应用同样重要如果函数参数类型是interface{}该函数可以接收任何类型的实参如果函数返回值类型是interface{}该函数可以返回任何类型的值。这是 Go 中泛型编程在早期版本1.18 之前最常用的替代手段也是fmt.Println、fmt.Sprintf等函数能接受任意参数的根本原因。仓库 reflection/main.go 中的show_interface_none()展示了空接口依次存放字符串、整数和布尔值的用法。接口的方法参数fmt.Stringer与error既然空接口能接收任意类型我们自然想到如何利用这一特性写出通吃任意参数的函数最常见的例子就是fmt.Println——你有没有注意过它能接受任意类型的参数查看 Go 标准库fmt的源码fmt是开源的可以看到如下定义type Stringer interface { String() string }这意味着任何实现了Stringer接口的类型都可以作为参数传给fmt.Println。仓库中 stringer_interface/main.go 给出了完整验证package main import ( fmt strconv ) type Human struct { name string age int phone string } // Human 实现 fmt.Stringer func (h Human) String() string { return Name: h.name , Age: strconv.Itoa(h.age) years, Contact: h.phone } func main() { Bob : Human{Bob, 39, 000-7777-XXX} fmt.Println(This Human is : , Bob) }输出结果不再是默认的结构体字面量格式而是我们自定义的Name:Bob, Age:39 years, Contact:000-7777-XXX。回到第 2.5 节的 Box 例子见 ch.2.5/box_example/main.go其中的Color类型同样实现了Stringer因此可以自定义颜色打印格式。对比下面两个调用fmt.Println(The biggest one is, boxes.BiggestsColor().String()) fmt.Println(The biggest one is, boxes.BiggestsColor())第一种显式调用String()第二种则依赖fmt内部对Stringer的自动识别两者输出相同——这正是自定义打印格式的惯用姿势。如果不实现该接口fmt.Println会用默认格式输出类型的内部结构。重要提醒如果某类型实现了error接口即拥有Error() string方法fmt会优先调用Error()方法此时就不必也不应再实现Stringer。接口变量中的具体类型comma-ok 断言与 type switch接口变量可以保存任何实现了该接口的类型那么问题来了如何得知接口变量中实际存储的具体类型Go 提供两种方式。方式一comma-ok 断言模式Go 提供语法value, ok : element.(T)element是接口变量T是我们要断言的类型value是断言成功后取出的具体值ok是布尔值表示断言是否成功。如果element确实是类型Tok为true否则为false。看仓库中 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 // 一个 int list[1] Hello // 一个 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.Println(list[%d] is of a different type, index) } } }这里定义了一个基于空接口的List类型用make(List, 3)创建切片后把int、string和自定义的Person混装其中再通过连续if-else加 comma-ok 断言逐一识别。这种模式简单直观但当需要判断的类型很多时if-else链会变得冗长此时应改用switch。方式二type switch 类型判断用switch重写上面的例子见 switch_type_check/main.gofor 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)特殊语法它是type switch专用形式。必须牢记element.(type)只能用在switch语句体内不能在switch之外使用——在switch之外判断类型只能使用 comma-ok 断言模式。接口嵌入Embedded InterfacesGo 的语法中处处体现了嵌入的设计哲学例如结构体中的匿名字段第 2.4 节。同样地接口也可以作为匿名成员嵌入其他接口称为接口嵌入其规则与匿名字段一致如果接口 A 嵌入了接口 B则 A 自动拥有 B 的全部方法。标准库container/heap中的定义就是经典范例type Interface interface { sort.Interface // 嵌入 sort.Interface Push(x interface{}) // Push 方法将元素推入堆 Pop() interface{} // Pop 方法弹出堆顶元素 }其中嵌入的sort.Interface展开后是type Interface interface { // Len 返回集合中元素的数量。 Len() int // Less 报告索引 i 的元素是否应排在 // 索引 j 的元素之前。 Less(i, j int) bool // Swap 交换索引 i 和 j 的元素。 Swap(i, j int) }因此heap.Interface实际包含 5 个方法Len()、Less()、Swap()、Push()、Pop()任何实现这 5 个方法的类型都能作为堆使用——只需实现排序协议 入栈出栈协议即可复用整个堆算法。另一个著名例子是io包中的io.ReadWriter// io.ReadWriter type ReadWriter interface { Reader Writer }io.Reader提供Read(p []byte) (n int, err error)io.Writer提供Write(p []byte) (n int, err error)两者合并即得到可读可写的流抽象。这正是 Go 标准库用小型接口组合出大型能力的精髓与本书后续 第 8 章 Web 服务 中io.Copy、http.ResponseWriter等基于接口组合的用法一脉相承。反射Reflection运行时获取类型信息反射用于在程序运行时获取类型与值的元信息。Go 通过标准库reflect提供这一能力。官方文章《The Laws of Reflection》系统阐述了其原理本节的实践版本见仓库 reflection/main.go。使用反射通常需要三个步骤。第一步把接口变量转换为 reflect 类型reflect.Type或reflect.Value视场景而定t : reflect.TypeOf(i) // 获取 i 的类型元数据用 t 读取各种属性 v : reflect.ValueOf(i) // 获取 i 的当前值用 v 修改这个值第二步从 reflect 值转换回需要的类型。例如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())v.Type()返回类型名v.Kind()返回底层种类这里与reflect.Float64比较v.Float()取回浮点值。第三步如果想修改 reflect 取出的值必须保证它是可修改的settable。这与传值/传引用指针的区别密切相关——下面的代码无法编译通过var x float64 3.4 v : reflect.ValueOf(x) v.SetFloat(7.1) // panic: reflect.Value.SetFloat using unaddressable value因为reflect.ValueOf(x)拿到的是x的一份拷贝对它赋值毫无意义。正确做法是先取地址再通过Elem()解引用后修改var x float64 3.4 p : reflect.ValueOf(x) v : p.Elem() v.SetFloat(7.1)p.Elem()返回p指向的变量的reflect.Value此时它才是可寻址、可修改的。修改后x的实际值也变为 7.1。反射是构建 ORM、序列化器、依赖注入等通用框架的基石但在日常业务代码中应谨慎使用因为反射会带来额外的运行时开销并且牺牲编译期类型检查。掌握了本节内容你就拥有了在运行时读懂和改写任意 Go 值的能力。小结本章围绕 Go 接口展开了五个核心知识点方法集驱动的隐式接口实现、接口值的多态语义、空接口与fmt.Stringer的自定义打印、comma-ok 断言与 type switch 的运行时类型识别、接口嵌入与reflect反射。建议对照仓库源码逐一运行验证ch.2.6/interface/main.go接口定义、赋值与[]Men多态切片ch.2.6/stringer_interface/main.go实现fmt.Stringer自定义输出ch.2.6/type_check/main.gocomma-ok 断言ch.2.6/switch_type_check/main.gotype switchch.2.6/reflection/main.go反射读取与修改值ch.2.5/box_example/main.goBox Color 实现Stringer的完整演示继续学习请进入下一节 并发或返回 目录 回顾 面向对象 一节。赞分享文档教程【免费下载链接】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 节 导读 本文文档教程在 v86 浏览器模拟器中引导 NetBSD 并运行 radare2dist/netbsd 完整排障实录在 v86 浏览器模拟器中引导 NetBSD 并运行 radare2 dist/netbsd 完整排障实录 在浏览器中直接运行 radare2 是一件很有吸引文档教程Go 语言 Web 应用错误处理实战从 error 接口设计到 HTTP 服务端容错架构build-web-application-with-golang 第 11.1 节详解Go 语言 Web 应用错误处理实战从 error 接口设计到 HTTP 服务端容错架构build web application with golang文档教程创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表