• Golang接口类型-上篇


    1、概述

    接口是计算机系统中多个组件共享的边界,不同的组件能够在边界上交换信息。接口的本质是引入一个新的中间层,调用方可以通过接口与具体实现分离,解除上下游的耦合,上层的模块不再需要依赖下层的具体模块,只需要依赖一个约定好的接口

    简单来说,Go语言中的接口就是一组方法的签名。接口是Go语言整个类型系统的基石,其他语言的接口是不同组件之间的契约的存在,对契约的实现是强制性的,必须显式声明实现了该接口,这类接口称之为“侵入式接口”。而Go语言的接口是隐式存在,只要实现了该接口的所有函数则代表已经实现了该接口,并不需要显式的接口声明

    接口的比喻
    ​一个常见的例子,电脑上只有一个USB接口。这个USB接口可以接MP3、数码相机、摄像头、鼠标、键盘等。所有的上述硬件都可以公用这个接口,有很好的扩展性,该USB接口定义了一种规范,只要实现了该规范,就可以将不同的设备接入电脑,而设备的改变并不会对电脑本身有什么影响(低耦合)

    接口表示调用者和设计者的一种约定,在多人合作开发同一个项目时,事先定义好相互调用的接口可以大大提高开发的效率。接口是用类来实现的,实现接口的类必须严格按照接口的声明来实现接口提供的所有功能。有了接口,就可以在不影响现有接口声明的情况下,修改接口的内部实现,从而使兼容性问题最小化

    2、接口的隐式实现

    Java中实现接口需要显式地声明接口并实现所有方法,而在Go中实现接口的所有方法就隐式地实现了接口
    定义接口需要使用interface关键字,在接口中只能定义方法签名,不能包含成员变量,例如

    type error interface {
    	Error() string
    }
    

    如果一个类型需要实现error接口,那么它只需要实现Error() string方法,下面的RPCError结构体就是 error 接口的一个实现

    type RPCError struct {
    	Code    int64
    	Message string
    }
    
    func (e *RPCError) Error() string {
    	return fmt.Sprintf("%s, code=%d", e.Message, e.Code)
    }
    

    会发现上述代码根本就没有error接口的影子,这正是因为Go语言中接口的实现都是隐式的

    3、接口定义和声明

    接口是自定义类型,是对其他类型行为的抽象(定义一个接口类型,把其他类型的值赋值给自定义的接口)

    接口定义使用interface标识,声明了一系列的函数签名(函数名、函数参数、函数返回值)在定义接口时可以指定接口名称,在后续声明接口变量时使用

    声明接口变量只需要定义变量类型为接口名,此时变量被初始化为nil

    package main
    
    import "fmt"
    
    type Sender interface {
    	Send(to string, msg string) error
    	SendAll(tos []string, msg string) error
    }
    
    func main()  {
    	var sender Sender
    	fmt.Printf("%T %v
    ", sender, sender)  // <nil> <nil>
    }
    

    4、接口类型赋值

    为接口类型方法赋值,一般是定义一个结构体,需要保证结构体方法(方法名、参数)均与接口中定义相同

    package main
    
    import "fmt"
    
    type Sender interface {
    	Send(to string, msg string) error
    	SendAll(tos []string, msg string) error
    }
    
    type EmailSender struct {
    }
    
    func (s EmailSender) Send(to, msg string) error {
    	fmt.Println("发送邮件给:", to, ",消息内容是:", msg)
    	return nil
    }
    
    func (s EmailSender) SendAll(tos []string, msg string) error {
    	for _, to := range tos {
    		s.Send(to, msg)
    	}
    	return nil
    }
    
    func main() {
    	var sender Sender = EmailSender{}
    	fmt.Printf("%T %v
    ", sender, sender) // <nil> <nil>
    	sender.Send("geek", "早上好")
    	sender.SendAll([]string{"aa","bb"}, "中午好")
    }
    

    使用接口的好处,概念上可能不好理解,来一个实际例子

    package main
    
    import "fmt"
    
    type Sender interface {
    	Send(to string, msg string) error
    	SendAll(tos []string, msg string) error
    }
    
    type EmailSender struct {
    }
    
    func (s EmailSender) Send(to, msg string) error {
    	fmt.Println("发送邮件给:", to, ",消息内容是:", msg)
    	return nil
    }
    
    func (s EmailSender) SendAll(tos []string, msg string) error {
    	for _, to := range tos {
    		s.Send(to, msg)
    	}
    	return nil
    }
    
    type SmsSender struct {
    }
    
    func (s SmsSender) Send(to, msg string) error {
    	fmt.Println("发送短信给:", to, ", 消息内容是:", msg)
    	return nil
    }
    
    func (s SmsSender) SendAll(tos []string, msg string) error {
    	for _, to := range tos {
    		s.Send(to, msg)
    	}
    	return nil
    }
    
    //func do(sender EmailSender) {
    func do(sender Sender) {
    	sender.Send("领导", "工作日志")
    }
    
    func main() {
    	var sender Sender = EmailSender{}
    	fmt.Printf("%T %v
    ", sender, sender) // <nil> <nil>
    	sender.Send("geek", "早上好")
    	sender.SendAll([]string{"aa","bb"}, "中午好")
    	do(sender)
    	sender = SmsSender{}
    	do(sender)
    }
    

    按照上面的示例,最后定义变量sender为接口类型Sender,调用接口方法时,只需要指定接口类型对应的结构体是什么,因为在定义接口时,已经声明了此接口实现了SendSendAll两个方法

    var sender Sender = EmailSender{}
    // 或
    var sender Sender = SmsSender{}
    // 单独定义go函数调用
    func do(sender Sender) {
    	sender.Send("领导", "工作日志")
    }
    

    如果没有接口,那么最终调用时,还需要对应上其具体的结构体类型,写法为

    var sender EmailSender = EmailSender{}
    // 或
    var sender SmsSender = SmsSender{}
    // 单独定义go函数调用
    func do(sender EmailSender) {
    // func do(sender SmsSender) {
    	sender.Send("领导", "工作日志")
    }
    

    很明显,前者使用接口定义变量,在传参时也使用接口类型定义,在使用上更为简单,仅仅只需要调整初始化的结构体类型即可

    5、接口类型对象

    当自定义类型实现了接口类型中声明的所有函数时,则该类型的对象可以赋值给接口变量,并使用接口变量调用实现的接口

    • 方法接收者全为值类型
      如上面的例子

    • 方法接收者全为指针类型

    package main
    
    import "fmt"
    
    type Sender interface {
    	Send(to string, msg string) error
    	SendAll(tos []string, msg string) error
    }
    
    type SmsSender struct {
    }
    
    func (s *SmsSender) Send(to, msg string) error {
    	fmt.Println("发送短信给:", to, ", 消息内容是:", msg)
    	return nil
    }
    
    func (s *SmsSender) SendAll(tos []string, msg string) error {
    	for _, to := range tos {
    		s.Send(to, msg)
    	}
    	return nil
    }
    
    func do(sender Sender) {
    	sender.Send("领导", "工作日志")
    }
    
    func main() {
    	var sender Sender = &SmsSender{}  // 指针类型
    	do(sender)
    }
    
    • 方法接收者既有值类型又有指针类型

    WechatSendersendsendAllsend有指针和值,sendAll只有指针,因此初始化的时候只能用指针,不能用值

    package main
    
    import "fmt"
    
    type Sender interface {
    	Send(to string, msg string) error
    	SendAll(tos []string, msg string) error
    }
    
    type WechatSender struct {
    }
    
    // Send 接收者为值对象
    func (s WechatSender) Send(to, msg string) error {
    	fmt.Println("发送微信给:", to, ", 消息内容是:", msg)
    	return nil
    }
    
    // SendAll 接收者为指针对象
    func (s *WechatSender) SendAll(tos []string, msg string) error {
    	for _, to := range tos {
    		s.Send(to, msg)
    	}
    	return nil
    }
    
    //func do(sender EmailSender) {
    func do(sender Sender) {
    	sender.Send("领导", "工作日志")
    }
    
    func main() {
    	var sender Sender = &WechatSender{}
    	do(sender)
    }
    

    当接口(A)包含另外一个接口(B)中声明的所有函数时(A接口函数是B接口函数的父集,B是A的子集),接口(A)的对象也可以赋值给其子集的接口(B)变量

    package main
    
    import "fmt"
    
    type SignalSender interface {
    	Send(to, msg string) error
    }
    
    type Sender interface {
    	Send(to string, msg string) error
    	SendAll(tos []string, msg string) error
    }
    
    ...
    
    func main() {
    	var ssender SignalSender = sender  // 以接口的变量初始化另外一个接口
    	ssender.Send("aa", "你好")
    }
    

    若两个接口声明同样的函数签名,则这两个接口完全等价
    当类型和父集接口赋值给接口变量时,只能调用接口变量定义接口中声明的函数(方法)

    6、接口应用举例

    实际的生产例子,可以加深对接口的理解。例如多个数据源推送和查询数据

    package main
    
    import (
    	"fmt"
    	"log"
    )
    
    /*
    1、多个数据源
    2、query方法查询数据
    3、pushdata方法写入数据
     */
    
    type DataSource interface {
    	PushData(data string)
    	QueryData(name string) string
    }
    
    type redis struct {
    	Name string
    	Addr string
    }
    
    func (r *redis) PushData (data string) {
    	log.Printf("pushdata,name:%s,data:%s
    ", r.Name,data)
    }
    func (r *redis) QueryData (name string) string {
    	log.Printf("querydata,name:%s,data:%s
    ", r.Name,name)
    	return name + "redis"
    }
    
    type kafka struct {
    	Name string
    	Addr string
    }
    
    func (k *kafka) PushData (data string) {
    	log.Printf("pushdata,name:%s,data:%s
    ", k.Name,data)
    }
    func (k *kafka) QueryData (name string) string {
    	log.Printf("querydata,name:%s,data:%s
    ", k.Name,name)
    	return name + "kafka"
    }
    
    var Dm = make(map[string]DataSource)
    
    func main()  {
    	r:=redis{
    		Name: "redis",
    		Addr: "127.0.0.1",
    	}
    	k:=kafka{
    		Name:"kafka",
    		Addr:"192.169.0.1",
    	}
    	// 注册数据源到承载的容器中
    	Dm["redis"] = &r
    	Dm["kafka"] = &k
    	// 推送数据
    	for i:=0;i<5;i++{
    		key:=fmt.Sprintf("key_%d", i)
    		for _,ds:=range Dm{
    			ds.PushData(key)
    		}
    	}
    	// 查询数据
    	for i:=0;i<5;i++{
    		key:=fmt.Sprintf("key_%d", i)
    		//r:=Dm["redis"]
    		//r.QueryData(key)
    		for _,ds:=range Dm{
    			res:=ds.QueryData(key)
    			log.Printf("query_from_ds,res:%s", res)
    		}
    	}
    }
    

    参考:https://draveness.me/golang/docs/part2-foundation/ch04-basic/golang-interface/

    See you ~

    关注公众号加群,更多原创干货与你分享 ~

  • 相关阅读:
    前端战五渣学前端——跨域
    CSS3学习笔记
    Vue工程化入口文件main.js中Vue.config.productionTip = false含义
    CSS选择器有哪几种?举例轻松理解CSS选择器
    研究生综合英语 作文 作业
    tomcat部署项目的方法
    【Winfrom-无边框窗体】Winform如何拖动无边框窗体?
    C#调用默认浏览器打开网页的几种方法
    CefSharp在高DPI的屏幕上出现黑边(winform)
    c# 关于mongo bson转json的问题
  • 原文地址:https://www.cnblogs.com/ssgeek/p/15433859.html
Copyright © 2020-2023  润新知