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