如果在Java基础上学习到Go的接口时,很难不被绕晕进去,因为Go和Java在这里是完全不同的两种设计思维。
Java的接口设计:自上而下显式接口
在 Java 中,接口由服务提供方定义,调用方必须明确声明implements遵守这套规范才能使用服务
如下Java示例,定义接口Storage,其中含有一个抽象方法save
// Java: 服务提供方定义接口和实现
public interface Storage {
void save(String data);
}
如果有一个具体的实现类MySQLStorage想要实现这个接口方法save,那么必须显式声明implements
public 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隐式实现的接口,我就无语了——这跟不实现有什么区别呢?
当我已经满足接口定义的函数规范时,我就实现了接口。但实现接口的意义,不就是要让我们的函数满足接口定义的函数规范吗,类似java?
Go给我的感觉是反过来了:既然是隐式的,那接口还有什么约束力?这不就失去了接口作为‘契约’的意义了吗?
其实,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的设计美感(也可能是我疯了)