Java转Go的难点四:接口和适配器

如果在Java基础上学习到Go的接口时,很难不被绕晕进去,因为Go和Java在这里是完全不同的两种设计思维。

Java的接口设计:自上而下显式接口

在 Java 中,接口由服务提供方定义,调用方必须明确声明implements遵守这套规范才能使用服务

如下Java示例,定义接口Storage,其中含有一个抽象方法save

java 复制代码
// Java: 服务提供方定义接口和实现
public interface Storage {
    void save(String data);
}

如果有一个具体的实现类MySQLStorage想要实现这个接口方法save,那么必须显式声明implements

java 复制代码
public class MySQLStorage implements Storage {
    @Override
    public void save(String data) {
        System.out.println("Saving to MySQL");
    }
}

Go的接口设计:自下而上隐式接口

隐式接口,又叫做非侵入式接口、鸭子类型接口。

Go的接口是调用方(消费者)自己定义的,实现类根本不知道接口的存在。实现类不需要知道有哪些接口在使用它,多个调用方可以根据自己的需求定义不同的接口,只要实现类的方法签名匹配,就能被自动满足

go接口设计规范跟"鸭子类型"完全一致:如果一个对象走起来像鸭子,叫起来是鸭子,那它就是鸭子

鸭子类型的核心思想:不关心对象是什么类型,只关心它有什么方法 。换句话说,只要具体类型的方法签名与调用方定义的接口一致(方法个数、方法名、参数列表、返回值列表完全匹配),那么就看做提供方实现了调用方的接口

如果你读懂了上句话,那肯定有非常多的地方想不通,别急,下一小节介绍,我们先来看一个示例------

如下Go示例,服务提供方写自己的方法即可,对接口是无感的

go 复制代码
type MySQLStorage struct{}

func (m MySQLStorage) Save(data string) {
    fmt.Println("Saving to MySQL")
}

那调用方是怎么适配的呢?这里先放一下代码,至于为什么这么写可以适配,我们后面再解释。

go 复制代码
// 调用方定义自己"需要"什么:我只需要一个能 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() 方法跟你的代码同名。

java 复制代码
// 你的项目代码:定义规范
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)包装一层。

java 复制代码
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)类,把第三方库包装起来,使其满足我们的要求。这就产生了冗余的"胶水代码"

java 复制代码
// 你被迫写的胶水代码
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:同类自动满足

假设你引入了同样的第三方阿里云包,他的源码你依然不能改

go 复制代码
// 第三方包:aliyun (你不能改源码,人家也不知道你的项目存在)
package aliyun

type SmsClient struct {}

// 它恰好提供了一个 Send 方法
func (c SmsClient) Send(message string) {
    fmt.Println("阿里云发送短信:", message)
}

现在看你自己的业务代码。在 Go 里,接口是调用方(消费者,也就是 AlertCenter)根据自己的需要定义的:

go 复制代码
// 你的项目代码 (消费者视角)
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、函数、接口类型等

代码举例:

go 复制代码
// 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中的方法(函数)可以挂载到任意==类型==上

那么我们就可以把函数挂载到函数类型上,比如下列代码

go 复制代码
// 先声明一个函数类型
type PrinterFunc func(msg string)
// 再给这个函数类型挂载方法
func (f PrinterFunc) ePrint(msg string) {
    f(msg) 
}

知道了这些知识,我们就可以优雅的写出Go的转接代码了

假设你要写代码去发送一个信息,你的报警中心类AlertCenter的方法send里期望接收一个实现了 MessageSender 接口的对象。

你引入了阿里的第三方库 AliyunClient,它里面有一个相同功能的方法 Alisend(msg String, isOnce bool)但方法名和参数都不同

go 复制代码
// 你的项目代码
package myproject

type MessageSender interface {
    Send(message string)
}

// 报警中心只依赖接口
func Alert(sender MessageSender, msg string) {
    sender.Send("【紧急报警】" + msg)
}
go 复制代码
// 第三方包:aliyun (你不能改源码,人家也不知道你的项目存在)
package aliyun

type SmsClient struct {}

// 它恰提供了一个 不同名,不同参数的方法
func (c SmsClient) AliSend(message string, isOnce bool) {
  fmt.Println("阿里云发送短信:", message, "isOnce:", isOnce)
}

没关系,根据我们的知识,我们可以先为第三方库定义好他们的函数类型:

go 复制代码
type SenderFunc func(msg string, isOnce bool)

再给这个函数类型挂载符合我们类的方法,注意实际执行方法的还是第三方库

go 复制代码
func (this SenderFunc) Send(msg string){
  this(msg, true)
}

现在,我们只需要创建好阿里云的实例,直接强制转换即可

go 复制代码
client := aliyun.SmsClient{}
Alert(SenderFunc(client.AliSend)) // go特性,client.AliSend是一个方法值,类型是 func(msg string, isOnce bool)

现在,我们已经把目标方法包装为我们需要的方法了!最终代码为:

go 复制代码
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的设计美感(也可能是我疯了)

相关推荐
努力的小郑2 小时前
Canal 不难,难的是用好:从接入到治理
后端·mysql·性能优化
Victor3562 小时前
MongoDB(87)如何使用GridFS?
后端
Victor3562 小时前
MongoDB(88)如何进行数据迁移?
后端
小红的布丁3 小时前
单线程 Redis 的高性能之道
redis·后端
GetcharZp3 小时前
Go 语言只能写后端?这款 2D 游戏引擎刷新你的认知!
后端
宁瑶琴4 小时前
COBOL语言的云计算
开发语言·后端·golang
普通网友5 小时前
阿里云国际版服务器,真的是学生党的性价比之选吗?
后端·python·阿里云·flask·云计算
IT_陈寒5 小时前
Vue的这个响应式问题,坑了我整整两小时
前端·人工智能·后端
Soofjan6 小时前
Go 内存回收-GC 源码1-触发与阶段
后端
shining6 小时前
[Golang]Eino探索之旅-初窥门径
后端