前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Golang interface 接口详细原理和使用技巧

Golang interface 接口详细原理和使用技巧

作者头像
Allen.Wu
发布于 2023-03-01 10:30:45
发布于 2023-03-01 10:30:45
1.6K00
代码可运行
举报
运行总次数:0
代码可运行

Golang interface 接口详细原理和使用技巧

一、Go interface 介绍

interface 在 Go 中的重要性说明

interface 接口在 Go 语言里面的地位非常重要,是一个非常重要的数据结构,只要是实际业务编程,并且想要写出优雅的代码,那么必然要用上 interface,因此 interface 在 Go 语言里面处于非常核心的地位。

我们都知道,Go 语言和典型的面向对象的语言不太一样,Go 在语法上是不支持面向对象的类、继承等相关概念的。但是,并不代表 Go 里面不能实现面向对象的一些行为比如继承、多态,在 Go 里面,通过 interface 完全可以实现诸如 C++ 里面的继承 和 多态的语法效果。

interface 的特性

Go 中的 interface 接口有如下特性:

  • • 关于接口的定义和签名
    • • 接口是一个或多个方法签名的集合,接口只有方法声明,没有实现,没有数据字段,只要某个类型拥有该接口的所有方法签名,那么就相当于实现了该接口,无需显示声明了哪个接口,这称为 Structural Typing。
    • • interface 接口可以匿名嵌入其他接口中,或嵌入到 struct 结构中
    • • 接口可以支持匿名字段方法
  • • 关于接口赋值
    • • 只有当接口存储的类型和对象都为 nil 时,接口才等于 nil
    • • 一个空的接口可以作为任何类型数据的容器
    • • 如果两个接口都拥有相同的方法,那么它们就是等同的,任何实现了他们这个接口的对象之间,都可以相互赋值
    • • 如果某个 struct 对象实现了某个接口的所有方法,那么可以直接将这个 struct 的实例对象直接赋值给这个接口类型的变量。
  • • 关于接口嵌套,Go 里面支持接口嵌套,但是不支持递归嵌套
  • • 通过接口可以实现面向对象编程中的多态的效果

interface 接口和 reflect 反射

在 Go 的实现里面,每个 interface 接口变量都有一个对应 pair,这个 pair 中记录了接口的实际变量的类型和值(value, type),其中,value 是实际变量值,type 是实际变量的类型。任何一个 interface{} 类型的变量都包含了2个指针,一个指针指向值的类型,对应 pair 中的 type,这个 type 类型包括静态的类型 (static type,比如 int、string...)和具体的类型(concrete type,interface 所指向的具体类型),另外一个指针指向实际的值,对应 pair 中的 value。

interface 及其 pair 的存在,是 Go 语言中实现 reflect 反射的前提,理解了 pair,就更容易理解反射。反射就是用来检测存储在接口变量内部(值value;类型concrete type) pair 对的一种机制。

二、Go 里面为啥偏向使用 Interface

Go 里面为啥偏向使用 Interface 呢? 主要原因有如下几点:

可以实现泛型编程(虽然 Go 在 1.18 之后已经支持泛型了)

在 C++ 等高级语言中使用泛型编程非常的简单,但是 Go 在 1.18 版本之前,是不支持泛型的,而通过 Go 的接口,可以实现类似的泛型编程,如下是一个参考示例

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    package sort

    // A type, typically a collection, that satisfies sort.Interface can be
    // sorted by the routines in this package.  The methods require that the
    // elements of the collection be enumerated by an integer index.
    type Interface interface {
        // Len is the number of elements in the collection.
        Len() int
        // Less reports whether the element with
        // index i should sort before the element with index j.
        Less(i, j int) bool
        // Swap swaps the elements with indexes i and j.
        Swap(i, j int)
    }
    
    ...
    
    // Sort sorts data.
    // It makes one call to data.Len to determine n, and O(n*log(n)) calls to
    // data.Less and data.Swap. The sort is not guaranteed to be stable.
    func Sort(data Interface) {
        // Switch to heapsort if depth of 2*ceil(lg(n+1)) is reached.
        n := data.Len()
        maxDepth := 0
        for i := n; i > 0; i >>= 1 {
            maxDepth++
        }
        maxDepth *= 2
        quickSort(data, 0, n, maxDepth)
    }
    

Sort 函数的形参是一个 interface,包含了三个方法:Len(),Less(i,j int),Swap(i, j int)。使用的时候不管数组的元素类型是什么类型(int, float, string…),只要我们实现了这三个方法就可以使用 Sort 函数,这样就实现了“泛型编程”。

这种方式,我在闪聊项目里面也有实际应用过,具体案例就是对消息排序。 下面给一个具体示例,代码能够说明一切,一看就懂:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    type Person struct {
    Name string
    Age  int
    }
    
    func (p Person) String() string {
        return fmt.Sprintf("%s: %d", p.Name, p.Age)
    }
    
    // ByAge implements sort.Interface for []Person based on
    // the Age field.
    type ByAge []Person //自定义
    
    func (a ByAge) Len() int           { return len(a) }
    func (a ByAge) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
    func (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age }
    
    func main() {
        people := []Person{
            {"Bob", 31},
            {"John", 42},
            {"Michael", 17},
            {"Jenny", 26},
        }
    
        fmt.Println(people)
        sort.Sort(ByAge(people))
        fmt.Println(people)
    }

可以隐藏具体的实现

隐藏具体的实现,是说我们提供给外部的一个方法(函数),但是我们是通过 interface 接口的方式提供的,对调用方来说,只能通过 interface 里面的方法来做一些操作,但是内部的具体实现是完全不知道的。

例如我们常用的 context 包,就是这样设计的,如果熟悉 Context 具体实现的就会很容易理解。详细代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
        c := newCancelCtx(parent)
        propagateCancel(parent, &c)
        return &c, func() { c.cancel(true, Canceled) }
    }
    

可以看到 WithCancel 函数返回的还是一个 Context interface,但是这个 interface 的具体实现是 cancelCtx struct。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
        // newCancelCtx returns an initialized cancelCtx.
        func newCancelCtx(parent Context) cancelCtx {
            return cancelCtx{
                Context: parent,
                done:    make(chan struct{}),
            }
        }
        
        // A cancelCtx can be canceled. When canceled, it also cancels any children
        // that implement canceler.
        type cancelCtx struct {
            Context     //注意一下这个地方
        
            done chan struct{} // closed by the first cancel call.
            mu       sync.Mutex
            children map[canceler]struct{} // set to nil by the first cancel call
            err      error                 // set to non-nil by the first cancel call
        }
        
        func (c *cancelCtx) Done() <-chan struct{} {
            return c.done
        }
        
        func (c *cancelCtx) Err() error {
            c.mu.Lock()
            defer c.mu.Unlock()
            return c.err
        }
        
        func (c *cancelCtx) String() string {
            return fmt.Sprintf("%v.WithCancel", c.Context)
        }

尽管内部实现上下面三个函数返回的具体 struct (都实现了 Context interface)不同,但是对于使用者来说是完全无感知的。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    func WithCancel(parent Context) (ctx Context, cancel CancelFunc)    //返回 cancelCtx
    func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) //返回 timerCtx
    func WithValue(parent Context, key, val interface{}) Context    //返回 valueCtx

可以实现面向对象编程中的多态用法

interface 只是定义一个或一组方法函数,但是这些方法只有函数签名,没有具体的实现,这个 C++ 中的虚函数非常类似。在 Go 里面,如果某个数据类型实现 interface 中定义的那些函数,则称这些数据类型实现(implement)了这个接口 interface,这是我们常用的 OO 方式,如下是一个简单的示例

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    // 定义一个 SimpleLog 接口
    type SimpleLog interface {
        Print()
    }
    
    func TestFunc(x SimpleLog) {}
   
    // 定义一个 PrintImpl 结构,用来实现 SimpleLog 接口
    type PrintImpl struct {}
    // PrintImpl 对象实现了SimpleLog 接口的所有方法(本例中是 Print 方法),就说明实现了  SimpleLog 接口
    func (p *PrintImpl) Print() {
    
    }
    
    func main() {
        var p PrintImpl
        TestFunc(p)
    }

空接口可以接受任何类型的参数

空接口比较特殊,它不包含任何方法:interface{} ,在 Go 语言中,所有其它数据类型都实现了空接口,如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
var v1 interface{} = 1
var v2 interface{} = "abc"
var v3 interface{} = struct{ X int }{1}

因此,当我们给 func 定义了一个 interface{} 类型的参数(也就是一个空接口)之后,那么这个参数可以接受任何类型,官方包中最典型的例子就是标准库 fmt 包中的 Print 和 Fprint 系列的函数。

一个简单的定义示例方法如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    Persist(context context.Context, msg interface{}) bool

msg 可以为任何类型,如 pb.MsgInfo or pb.GroupMsgInfo,定义方法的时候可以统一命名模块,实现的时候,根据不同场景实现不同方法。

三、Go interface 的常见应用和实战技巧

interface 接口赋值

可以将一个实现接口的对象实例赋值给接口,也可以将另外一个接口赋值给接口。

通过对象实例赋值

将一个对象实例赋值给一个接口之前,要保证该对象实现了接口的所有方法。在 Go 语言中,一个类只需要实现了接口要求的所有函数,我们就说这个类实现了该接口,这个是非侵入式接口的设计模式,非侵入式接口一个很重要的优势就是可以免去面向对象里面那套比较复杂的类的继承体系。

在 Go 里面,面向对象的那套类的继承体系就不需要关心了,定义接口的时候,我们只需关心这个接口应该提供哪些方法,当然,按照 Go 的原则,接口的功能要尽可能的保证职责单一。而对应接口的实现,也就是接口的调用方,我们只需要知道这个接口定义了哪些方法,然后我们实现这些方法就可以了,这个也无需提前规划,调用方也无需关系是否有其他模块定义过类似的接口或者实现,只关注自身就行。

考虑如下示例:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
type Integer int
func (a Integer) Less(b Integer) bool {
    return a < b
}
func (a *Integer) Add(b Integer) {
    *a += b
}
type LessAdder interface { 
    Less(b Integer) bool 
    Add(b Integer)
}
var a Integer = 1
var b1 LessAdder = &a  //OK
var b2 LessAdder = a   //not OK

b2 的赋值会报编译错误,为什么呢?因为这个:The method set of any other named type T consists of all methods with receiver type T. The method set of the corresponding pointer type T is the set of all methods with receiver T or T (that is, it also contains the method set of T). 也就是说 *Integer 这个指针类型实现了接口 LessAdder 的所有方法,而 Integer 只实现了 Less 方法,所以不能赋值。

通过接口赋值
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
        var r io.Reader = new(os.File)
        var rw io.ReadWriter = r   //not ok
        var rw2 io.ReadWriter = new(os.File)
        var r2 io.Reader = rw2    //ok

因为 r 没有 Write 方法,所以不能赋值给rw。

interface 接口嵌套

io package 中的一个接口:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// ReadWriter is the interface that groups the basic Read and Write methods.
type ReadWriter interface {
    Reader
    Writer
}

ReadWriter 接口嵌套了 io.Reader 和 io.Writer 两个接口,实际上,它等同于下面的写法:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
type ReadWriter interface {
    Read(p []byte) (n int, err error) 
    Write(p []byte) (n int, err error)
}

注意,Go 语言中的接口不能递归嵌套,如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// illegal: Bad cannot embed itself
type Bad interface {
    Bad
}
// illegal: Bad1 cannot embed itself using Bad2
type Bad1 interface {
    Bad2
}
type Bad2 interface {
    Bad1
}

interface 强制类型转换

ret, ok := interface.(type) 断言

在 Go 语言中,可以通过 interface.(type) 的方式来对一个 interface 进行强制类型转换,但是如果这个 interface 被转换为一个不包含指定类型的类型,那么就会出现 panic 。因此,实战应用中,我们通常都是通过 ret, ok := interface.(type) 这种断言的方式来优雅的进行转换,这个方法中第一个返回值是对应类型的值,第二个返回值是类型是否正确,只有 ok = true 的情况下,才说明转换成功,最重要的是,通过这样的转换方式可以避免直接转换如果类型不对的情况下产生 panic。

如下是一个以 string 为类型的示例:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
str, ok := value.(string)
if ok {
    fmt.Printf("string value is: %q\n", str)
} else {
    fmt.Printf("value is not a string\n")
}

如果类型断言失败,则str将依然存在,并且类型为字符串,不过其为零值,即一个空字符串。
switch x.(type) 断言

查询接口类型的方式为:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
switch x.(type) {
    // cases :
}

示例如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
var value interface{} // Value provided by caller.
switch str := value.(type) {
case string:
    return str //type of str is string
case int: 
    return int //type of str is int
}
语句switch中的value必须是接口类型,变量str的类型为转换后的类型。

interface 与 nil 的比较

interface 与 nil 的比较是挺有意思的,例子是最好的说明,如下例子:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
package main

import (
    "fmt"
    "reflect"
)

type State struct{}

func testnil1(a, b interface{}) bool {
    return a == b
}

func testnil2(a *State, b interface{}) bool {
    return a == b
}

func testnil3(a interface{}) bool {
    return a == nil
}

func testnil4(a *State) bool {
    return a == nil
}

func testnil5(a interface{}) bool {
    v := reflect.ValueOf(a)
    return !v.IsValid() || v.IsNil()
}

func main() {
    var a *State
    fmt.Println(testnil1(a, nil))
    fmt.Println(testnil2(a, nil))
    fmt.Println(testnil3(a))
    fmt.Println(testnil4(a))
    fmt.Println(testnil5(a))
}

运行后返回的结果如下

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
false
false
false
true
true

为什么是这个结果?

因为一个 interface{} 类型的变量包含了2个指针,一个指针指向值的类型,另外一个指针指向实际的值。对一个 interface{} 类型的 nil 变量来说,它的两个指针都是0;但是 var a State 传进去后,指向的类型的指针不为0了,因为有类型了, 所以比较为 false。 interface 类型比较, 要是两个指针都相等,才能相等。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-11-29,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 后端系统和架构 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
linux中如何添加用户并赋予root权限
passwd: all authentication tokens updated successfully.
用户8965210
2021/10/14
9.7K0
linux下添加用户并赋予root权限
passwd: all authentication tokens updated successfully.
用户1685462
2021/07/27
9.7K0
使用了腾讯云Ubuntu系统,但是没有root权限怎么办?
(2)、设置成功后,终端会提示password updated successfully,此时输入以下指令,回车,进入SSH配置页面,如下图所示
用户6948990
2025/04/11
2140
使用了腾讯云Ubuntu系统,但是没有root权限怎么办?
Ubuntu系统获取和禁用root权限教程
sudo passwd root Enter new UNIX password: (在这输入你的密码) Retype new UNIX password: (确定你输入的密码) passwd: password updated successfully
会长君
2023/04/25
1.6K0
为ubuntu操作系统增加root用户
该文介绍了如何为ubuntu操作系统增加root用户。首先需要开启root账户,然后设置root密码,最后测试root账户。具体步骤包括打开终端,使用sudo gedit命令打开配置文件,在文件中添加greeter-show-manual-login=true,保存并关闭文件。之后需要重启计算机以使更改生效。最后,需要重新登录时使用root账户。","author":"赵阳好", "content":"该文介绍了如何为ubuntu操作系统增加root用户。首先需要开启root账户,然后设置root密码,最后测试root账户。具体步骤包括打开终端,使用sudo gedit命令打开配置文件,在文件中添加greeter-show-manual-login=true,保存并关闭文件。之后需要重启计算机以使更改生效。最后,需要重新登录时使用root账户。", "title":"为ubuntu操作系统增加root用户
别先生
2017/12/29
2.6K0
为ubuntu操作系统增加root用户
ubuntu root默认密码(初始密码)
ubuntu安装好后,root初始密码(默认密码)不知道,需要设置。 1、先用安装时候的用户登录进入系统 2、输入:sudo passwd  按回车 3、输入新密码,重复输入密码,最后提示passwd:password updated sucessfully 此时已完成root密码的设置 4、输入:su root 切换用户到root试试.......
似水的流年
2019/12/08
11.3K0
Ubuntu安装完后设置root密码
安装完Ubuntu 14.04后默认是没有主动设置root密码的,也就无法进入根用户。
用户8705036
2021/06/08
2.6K0
Linux 修改用户密码「建议收藏」
Linux修改密码用 passwd 命令,用root用户运行passwd ,passwd user_name可以设置或修改任何用户的密码,普通用户运行passwd只能修改它自己的密码。
全栈程序员站长
2022/09/07
5.8K0
远程Ubuntu系统时获取Root权限
在日常使用云服务器时,经常会遇到服务器无法获取root权限,特别是Ubuntu系统,系统在开始时,会让你采用你自定义的一个名称,类似windows10让你自己创建一个账号而并不是使用Administrator账号一个道理,但是往往自己创建的账号并没有什么用,特别是开发者在开发项目的时候,往往会导致权限不够,无法进行编辑,接下来我就给大家解决这个问题吧!
Meng小羽
2019/12/23
5.5K6
启用某些Linux发行版的root帐号
跟了我 5 年多的本本已步入花甲,CPU 严重老化,运行 Windows 异常吃力,于是考虑换成 Linux 试试。忙活了一天,测试了 2 个“家用”Linux 发行版,一个是深度的 Linux Deepin 2013,另一个是雨林木风的 StartOS 5.1。在测试过程中也遇到一些有用的经验,现在就一一记录一下。 这些发行版和 ubuntu 一样,root 帐号都是停用的,在我安装完后,发现进行一些操作时,提示权限不足。其实我知道可以使用 sudo 来临时获取 root 权限,但是毕竟想一劳永逸,于是就
张戈
2018/03/23
2.9K0
启用某些Linux发行版的root帐号
ubantu下su命令Authentication failure失败的解决方式
Ubuntu安装后,root用户默认是被锁定了的,不允许登录,也不允许 su 到 root 。 可以使用: sudo passwd 来重新设置root密码,后即可登陆root。 ortonwu@ubuntu:/etc/vim$ sudo passwd Enter new UNIX password: Retype new UNIX password: passwd: password updated successfully
Tencent JCoder
2018/07/02
1.4K0
如何给Ubuntu设置root账户?
昨天我们讲解了Java的构造函数重载以及和普通函数的一些区别, 那么今天来玩点别的,比如最新的Ubuntu系统在进入系统后并没有给我们设置root账号,那么今天就让小编来带大家演示一下。
小Bob来啦
2021/11/04
2.2K0
如何给Ubuntu设置root账户?
ubuntu中root和普通用户切换方法
ubuntu登录后,默认是普通用户权限,那么普通用户权限和root权限如何切换呢,下面总结下它们之间如何切换。
一个会写诗的程序员
2022/09/30
8.3K0
第二章,ubuntu系统的查看防火墙,切换root用户,设置固定ip、系统时间等
第一次接触ubuntu系统,之前用的都是centos系统,因此也需要知道ubuntu的基本操作,跟centos的差别还是很大的。
全栈程序员站长
2022/08/05
2.1K0
第二章,ubuntu系统的查看防火墙,切换root用户,设置固定ip、系统时间等
[Linux]Ubuntu设置root密码并解决xShell连接问题
在终端输入命令 sudo passwd,然后输入当前用户的密码,需要确认两次。 也可以输入命令sudo passwd root 进行设置。
祥知道
2020/03/10
2.5K0
centos 7系统下安装laravel运行环境的步骤详解
前言 因为最近在学习linux,而最好的学习就是实践,/【一个开发人员,能懂服务器量好,反之一个服务器维护人员,也应该懂开发】/学习linux同时安装LAMP的环境搭配,跑了度娘都没找到我想要的文章。那我就简单的写写我在centos7下安装laravel的安装过程。 网络设置 ping 114.114.114.144 网络连接失败,将虚拟机的网络适配器改成桥接模式(自动),然后设置开启启动
用户2323866
2021/07/01
1.7K0
CentOS普通用户添加管理员权限 原
1、添加用户,首先用adduser命令添加一个普通用户,命令如下: #adduser keaising//添加一个名为keaising的用户 #passwd ljl //修改密码 Changing password for user keaising. New UNIX password: //在这里输入新密码 Retype new UNIX password: //再次输入新密码 passwd: all authentication tokens updated successfully. 2、赋予root权限 方法一: 修改 /etc/sudoers 文件,找到下面一行,把前面的注释(#)去掉,最终结果为: ## Allows people in group wheel to run all commands %wheel ALL=(ALL) ALL 然后修改用户,使其属于root组(wheel),命令如下: #usermod -g root keaising 修改完毕,现在可以用keaising帐号登录,然后用命令 su – ,即可获得root权限进行操作。
拓荒者
2019/03/11
4K0
linux普通用户获取管理员权限
passwd: all authentication tokens updated successfully.
用户8826052
2022/03/02
4.2K0
【linux命令讲解大全】211.Linux系统命令之passwd的用法详解
passwd命令用于设置用户的认证信息,包括用户密码、密码过期时间等。系统管理者则能用它管理系统用户的密码。只有管理者可以指定用户名称,一般用户只能变更自己的密码。
全栈若城
2024/03/02
4660
每天学一个 Linux 命令(10):passwd
https://github.com/mingongge/Learn-a-Linux-command-every-day
民工哥
2021/01/12
1.1K0
每天学一个 Linux 命令(10):passwd
推荐阅读
相关推荐
linux中如何添加用户并赋予root权限
更多 >
LV.5
这个人很懒,什么都没有留下~
目录
  • Golang interface 接口详细原理和使用技巧
    • 一、Go interface 介绍
      • interface 在 Go 中的重要性说明
      • interface 的特性
      • interface 接口和 reflect 反射
    • 二、Go 里面为啥偏向使用 Interface
      • 可以实现泛型编程(虽然 Go 在 1.18 之后已经支持泛型了)
      • 可以隐藏具体的实现
      • 可以实现面向对象编程中的多态用法
      • 空接口可以接受任何类型的参数
    • 三、Go interface 的常见应用和实战技巧
      • interface 接口赋值
      • interface 接口嵌套
      • interface 强制类型转换
      • interface 与 nil 的比较
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档