
如果在Java基础上学习到Go的接口时很难不被绕晕进去因为Go和Java在这里是完全不同的两种设计思维。Java的接口设计自上而下显式接口在 Java 中接口由服务提供方定义调用方必须明确声明implements遵守这套规范才能使用服务如下Java示例定义接口Storage其中含有一个抽象方法save// Java: 服务提供方定义接口和实现 public interface Storage { void save(String data); }如果有一个具体的实现类MySQLStorage想要实现这个接口方法save那么必须显式声明implementspublic class MySQLStorage implements Storage { Override public void save(String data) { System.out.println(Saving to MySQL); } }Go的接口设计自下而上隐式接口隐式接口又叫做非侵入式接口、鸭子类型接口。Go的接口是调用方消费者自己定义的实现类根本不知道接口的存在。实现类不需要知道有哪些接口在使用它多个调用方可以根据自己的需求定义不同的接口只要实现类的方法签名匹配就能被自动满足go接口设计规范跟鸭子类型完全一致如果一个对象走起来像鸭子叫起来是鸭子那它就是鸭子鸭子类型的核心思想不关心对象是什么类型只关心它有什么方法。换句话说只要具体类型的方法签名与调用方定义的接口一致方法个数、方法名、参数列表、返回值列表完全匹配那么就看做提供方实现了调用方的接口如果你读懂了上句话那肯定有非常多的地方想不通别急下一小节介绍我们先来看一个示例——如下Go示例服务提供方写自己的方法即可对接口是无感的type MySQLStorage struct{} func (m MySQLStorage) Save(data string) { fmt.Println(Saving to MySQL) }那调用方是怎么适配的呢这里先放一下代码至于为什么这么写可以适配我们后面再解释。// 调用方定义自己需要什么我只需要一个能 Save 的东西 type Saver interface { Save(data string) } func ProcessData(s Saver, data string) { s.Save(data) } func main() { // MySQLStorage 隐式满足了 Saver直接传进去 ProcessData(MySQLStorage{}, hello) }Go隐式接口的意义在哪里既然没有强制的implements关键字Go 接口的意义在哪里呢我们知道Java通过implement实现接口实现后编辑器会自动检查方法的实现有没有问题帮助我们遵守接口规范。当我刚开始学习go隐式实现的接口我就无语了——这跟不实现有什么区别呢当我已经满足接口定义的函数规范时我就实现了接口。但实现接口的意义不就是要让我们的函数满足接口定义的函数规范吗类似javaGo给我的感觉是反过来了既然是隐式的那接口还有什么约束力这不就失去了接口作为‘契约’的意义了吗其实Go接口最大的意义在于解耦。因为接口是调用方定义的服务提供方无需依赖调用方的包彻底避免了循环依赖和包污染。Java接口的痛点假设你要写代码去发送一个信息你的报警中心类AlertCenter的方法send里期望接收一个实现了MessageSender接口的对象。你引入了阿里的第三方库AliyunClient它里面恰好有一个send()方法跟你的代码同名。// 你的项目代码定义规范 public interface MessageSender { void send(String message); } // 你的项目代码报警中心依赖这个接口 public class AlertCenter { public void alert(MessageSender sender, String msg) { sender.send(【紧急报警】 msg); } } // 第三方 SDK (你无法修改它的源码它没有写 implements MessageSender) public class AliyunSmsClient { // 恰好它也有一个一模一样的方法签名 public void send(String message) { System.out.println(阿里云发送短信 message); } }在 Java 中你无法直接把AliyunClient传给你的方法因为第三方库没有implements MessageSender。你必须写一个适配器类Adapter包装一层。public class Main { public static void main(String[] args) { AlertCenter center new AlertCenter(); AliyunSmsClient aliyunClient new AliyunSmsClient(); // 编译报错Incompatible types: AliyunSmsClient cannot be converted to MessageSender // Java 编译器说虽然它会发消息但它的户口本上没有写 implements MessageSender center.alert(aliyunClient, 服务器宕机啦); } }Java的解决方法必须写一个适配器Adapter类把第三方库包装起来使其满足我们的要求。这就产生了冗余的“胶水代码”// 你被迫写的胶水代码 public class AliyunSmsAdapter implements MessageSender { private AliyunSmsClient client new AliyunSmsClient(); Override public void send(String message) { client.send(message); // 纯粹为了类型匹配而做的转发 } } // 现在终于能用了 center.alert(new AliyunSmsAdapter(), 服务器宕机啦);Go接口的爽点1同类自动满足假设你引入了同样的第三方阿里云包他的源码你依然不能改// 第三方包aliyun (你不能改源码人家也不知道你的项目存在) package aliyun type SmsClient struct {} // 它恰好提供了一个 Send 方法 func (c SmsClient) Send(message string) { fmt.Println(阿里云发送短信, message) }现在看你自己的业务代码。在 Go 里接口是调用方消费者也就是AlertCenter根据自己的需要定义的// 你的项目代码 (消费者视角) package myproject import aliyun // 我只关心传进来的东西能不能 Send我不关心它叫啥也不关心它有没有声明 type MessageSender interface { Send(message string) } // 报警中心只依赖接口 func Alert(sender MessageSender, msg string) { sender.Send(【紧急报警】 msg) } func main() { client : aliyun.SmsClient{} // 奇迹时刻直接传进去没有任何适配器 // 编译器发现 aliyun.SmsClient 有 Send(string) 方法自动认定它就是 MessageSender Alert(client, 服务器宕机啦) }我们的详细拆解go是怎么适配的。首先我们自己的Alert方法需要传入一个MessageSender接口的实例然后调用这个示例的send方法那如何成为这个接口的实例呢根据go的隐式接口要求只要其中有形如send(message string)类型的方法即可你会发现我们的阿里云第三方库恰好满足这一点因此可以直接适配传入Alert方法Go接口的爽点2差异方法用函数适配刚刚的例子第三方库的方法声明跟我们的方法声明恰好一致了。如果不一致不是还得写适配器的胶水代码吗我的回答是确实还要适配但Go的适配非常优雅不需要定义笨重的结构体。Go的类型声明我们知道Go中能声明的类型的不只有结构体还有基本类型(int string)也可以是结构体、数组、切片、map、函数、接口类型等。代码举例// 1. 基本类型的别名/新类型 type MyInt int func (m MyInt) IsZero() bool { return m 0 } func main() { var x MyInt 10 println(x.IsZero()) // false } // 2. 切片类型 type IntSlice []int func (s IntSlice) Sum() int { total : 0 for _, v : range s { total v } return total } //3. 映射类型 type Dictionary map[string]string func (d Dictionary) Get(key string) (string, bool) { val, ok : d[key] return val, ok } //4. 函数类型 // 定义一个基于函数类型的新类型 type Handler func(string) string // 给这个函数类型添加方法 func (h Handler) LogAndCall(name string) string { fmt.Println(calling handler with, name) return h(name) } // 使用 func main() { var h Handler func(s string) string { return hello, s } result : h.LogAndCall(world) fmt.Println(result) // hello, world } //5. 接口类型虽然不常见但可以 type Greeter interface { Greet() string } type MyGreeter Greeter // 基于接口定义新类型 func (m MyGreeter) SayHi() { fmt.Println(m.Greet()) // 调用接口方法 }注意不能直接给一个已有的匿名函数或普通函数添加方法必须先定义一个新类型基于函数类型然后给这个新类型添加方法。此外我们还知道Go中的方法函数可以挂载到任意类型上那么我们就可以把函数挂载到函数类型上比如下列代码// 先声明一个函数类型 type PrinterFunc func(msg string) // 再给这个函数类型挂载方法 func (f PrinterFunc) ePrint(msg string) { f(msg) }知道了这些知识我们就可以优雅的写出Go的转接代码了假设你要写代码去发送一个信息你的报警中心类AlertCenter的方法send里期望接收一个实现了MessageSender接口的对象。你引入了阿里的第三方库AliyunClient它里面有一个相同功能的方法Alisend(msg String, isOnce bool)但方法名和参数都不同// 你的项目代码 package myproject type MessageSender interface { Send(message string) } // 报警中心只依赖接口 func Alert(sender MessageSender, msg string) { sender.Send(【紧急报警】 msg) }// 第三方包aliyun (你不能改源码人家也不知道你的项目存在) package aliyun type SmsClient struct {} // 它恰提供了一个 不同名不同参数的方法 func (c SmsClient) AliSend(message string, isOnce bool) { fmt.Println(阿里云发送短信, message, isOnce:, isOnce) }没关系根据我们的知识我们可以先为第三方库定义好他们的函数类型type SenderFunc func(msg string, isOnce bool)再给这个函数类型挂载符合我们类的方法注意实际执行方法的还是第三方库func (this SenderFunc) Send(msg string){ this(msg, true) }现在我们只需要创建好阿里云的实例直接强制转换即可client : aliyun.SmsClient{} Alert(SenderFunc(client.AliSend)) // go特性client.AliSend是一个方法值类型是 func(msg string, isOnce bool)现在我们已经把目标方法包装为我们需要的方法了最终代码为package myproject import ( fmt aliyun ) type MessageSender interface { Send(message string) } func Alert(sender MessageSender, msg string) { sender.Send(【报警】 msg) } // 1.为第三方库定义好他们的函数类型 type SenderFunc func(msg string, isOnce bool) // 2.给函数类型挂载符合我们类的方法 func (this SenderFunc) Send(msg string) { this(msg, true) // 当调用接口的 Send 时实际上执行了这个函数类型的实例 } func main() { client : aliyun.SmsClient{} // 直接把 client.AliSend 这个方法强制类型转换为 SenderFunc 类型。 // 因为 SenderFunc 实现了 MessageSender 接口所以它直接就可以传进去 Alert(SenderFunc(client.AliSend), 服务器宕机啦) // 或者你甚至可以直接传一个匿名函数进去进行适配,不用定义上述类型 Alert(SenderFunc(func(msg string, isOnce bool) { // 在这里你想怎么适配就怎么适配 client.AliSend(前缀_ msg, true) }), 数据库也挂啦) } // 第三方包aliyun package aliyun type SmsClient struct {} func (c SmsClient) AliSend(message string, isOnce bool) { fmt.Println(阿里云发送短信, message, isOnce:, isOnce) }总结当遇到方法名不一致时强如 Go 也得乖乖写适配。但这恰恰体现了 Go 和 Java 在系统设计上的核心差异Java 的接口是“中心化”的制定接口的人是老大。不管第三方组件有多好用只要第三方没有效忠老大implements你就得自己建个中转站Adapter 类来对接。Go 的接口是“去中心化”的接口是调用者用来描述自己需求的如果第三方刚好有这个能力方法一致直接拿来用零成本。如果第三方能力有但名字不对Go 提供了极其轻量级的方案比如像SenderFunc这样的函数类型转换让你用一行代码就能完成适配而不需要像 Java 那样去新建一个类文件。如果你深入 Go 的源码比如net/http包的http.HandlerFunc你会发现到处都是这种“为函数类型挂载方法”的骚操作看多了你会发现他有一种不同于Java的设计美感也可能是我疯了