V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
yunshangdetianya
V2EX  ›  Go 编程语言

请教各位 go 语言大佬一个问题

  •  
  •   yunshangdetianya · 2022-04-11 11:54:45 +08:00 · 3990 次点击
    这是一个创建于 962 天前的主题,其中的信息可能已经有所发展或是发生改变。
    go 语言中的接口与方法的区别
    结构体 A ,拥有方法 run
    结构体 B ,拥有方法 run
    接口 C ,内部有一个方法 run
    那么我调用 run 方法时候,可以直接 A.run ,或则 B.run 不就行了
    为何还要定义一个接口变量,然后实例化这个变量,再调用方法?这不是多此一举吗?伪代码如下
    var x C
    x=A{}
    x.run
    x=B{}
    x.run()
    我直接
    x :=A{}
    x.run()
    或者
    x :=B{}
    b.run()
    感觉弄一个接口不是多此一举吗?
    初学 go ,往大家多多解惑
    31 条回复    2022-04-12 10:19:17 +08:00
    johopig
        1
    johopig  
       2022-04-11 12:05:51 +08:00
    假如其他地方要使用到 run ,那只要参数定义为 C ,就可以根据实际情况使用 A 或 B 而不用修改参数了
    dzdh
        2
    dzdh  
       2022-04-11 12:21:17 +08:00
    type DoI interface { func run() }

    type DoSome struct { func run() {} }

    func ToDO(obj DoI) {}

    ToDO(&DoSome{})
    dzdh
        3
    dzdh  
       2022-04-11 12:21:38 +08:00
    想想适配器的设计模式
    zhangfeiwudi
        4
    zhangfeiwudi  
       2022-04-11 12:27:03 +08:00
    多态啊,唐老鸭,北京烤鸭,可达鸭,白马会所头牌鸭 都是鸭子。但是细分不同,所以需要通过接口统一规范,屏蔽掉底层实现逻辑的不同,统一使用一致的方法。

    再比如说 io 接口就是非常的好解释。 只要能实现 read write 都可以作为 io 参数传递,所以 网络是 io, 文件也是 io, 内存也是 io 。 其实这是面向对象的一些知识点,如果做过 java 或者 php 就会对这一块比较了解了
    seers
        5
    seers  
       2022-04-11 12:29:44 +08:00
    我也刚看到这个,一样的疑惑,不过我觉得可能是一种约束,如果能实现这个接口就必定能用接口的方法,一个高层一点的抽象
    Elaina
        6
    Elaina  
       2022-04-11 12:45:52 +08:00
    这个还是为了多态,最简单的一个例子,假设你有一个动物基类,拥有一个 run 方法,然后派生出了 n 个动物类(比如猫、狗、猴子等等),然后有一个函数,函数需要传入一个动物类,并在函数体内调用对应的 run 方法。这个时候,如果你不使用接口,则需要为每一个动物类都定义一个这样的方法,会产生大量冗余代码。
    yunshangdetianya
        7
    yunshangdetianya  
    OP
       2022-04-11 13:37:31 +08:00
    谢谢各位的指导,我也再去想想,只是感觉接口存在是为了管理方法而存在,在实际应用时候,还没搞懂到底在那些场景下使用
    dongtingyue
        8
    dongtingyue  
       2022-04-11 14:07:17 +08:00
    实际使用中的例子,win 和 linux 下读取文件列表可以抽象一个 list()方法,在不同地方使用的时候不需要判断是 a.list 还是 b.list 直接 x.list 至于判断在封装的时候处理一次就行。
    如果是简单的情况可以像你那样直接弄,确实没必要再封一层。
    yunshangdetianya
        9
    yunshangdetianya  
    OP
       2022-04-11 14:10:16 +08:00
    @dongtingyue 谢谢,我去体会下,可能使用的少,不是很明白
    BeautifulSoap
        10
    BeautifulSoap  
       2022-04-11 14:15:59 +08:00
    lz 你学 go 之前是一直在用动态类型语言吗( JS ,Python 之类的),如果是习惯了动态语言的,的确一开始即非常难以理解接口的用处的
    lostpg
        11
    lostpg  
       2022-04-11 14:23:03 +08:00 via Android
    接口的存在使得调用 run 的的上下文中不需要确切知道被调用者的具体类型,只需要知道被调用者满足接口约定,就可以使用接口所定义的行为。
    yunshangdetianya
        12
    yunshangdetianya  
    OP
       2022-04-11 14:25:21 +08:00
    @BeautifulSoap 是的,使用 python 的,所以才觉得疑惑
    yunshangdetianya
        13
    yunshangdetianya  
    OP
       2022-04-11 14:26:53 +08:00
    @lostpg 感觉接口像是类的实例化,但又不完全是,不知道我理解的对不对
    yunshangdetianya
        14
    yunshangdetianya  
    OP
       2022-04-11 14:27:36 +08:00
    @BeautifulSoap 有什么可以帮助理解的方法或者代码例子吗?
    lostpg
        15
    lostpg  
       2022-04-11 14:41:10 +08:00
    @yunshangdetianya #13

    > 感觉接口像是类的实例化

    说得不对,接口是 OOP 相关的概念,你可能需要相关的前置知识。
    yunshangdetianya
        16
    yunshangdetianya  
    OP
       2022-04-11 14:48:27 +08:00
    @lostpg 需要补一补那块知识,我去学习下
    soupu626
        17
    soupu626  
       2022-04-11 15:32:21 +08:00
    @yunshangdetianya 这个其实不是 go 的问题,是 OOP 的概念,可以看看 设计模式
    我理解接口最大的用处是解耦,面向接口编程,而不关心具体实现

    比如有个任务系统 TaskService ,在某些条件下需要发消息,然后 PushSerivce 给出了一个接口 push(param),提供了消息推送的能力
    这样 TaskSerivce 就只需要关心内部的逻辑,至于推送,不用关心细节,只要知道有个 push 接口,直接调就行。
    而对于 PushSerivce ,他只提供了一个标准接口,只要保证接口功能和定义正常,底层实现可以随意更换,比如换运营商,换推送渠道(短信、APP ),或者根据入参,路由到不同的实现类上(策略模式)
    这里的 PushService 就是你说的 C ,针对不同运营商、渠道的实现就是 A 和 B
    BeautifulSoap
        18
    BeautifulSoap  
       2022-04-11 15:43:08 +08:00   ❤️ 3
    @yunshangdetianya 直接在这里讲好了

    很正常的,编程先接触动态语言的话就会很难理解静态语言的接口到底有什么意义,ls 很多人说的其实都对,但都是站在静态语言使用者的角度在教你,你作为一个动态语言接触编程的人是没法轻松理解他们到底在说什么的。因为接口提供的动态能力是动态语言天生就有的,最先接触动态语言的话会把这种动态能力当成呼吸一样理所当然,所以没法理解静态语言这么大费周章是图什么

    所以要跟你讲清楚接口,那就要从 python 角度来帮你理解 —— 在 python 中(虽然 python 没接口),接口就相当于在你调用对象的方法时加了一道验证,限制了过于自由类型系统

    比如很下面这段 Python 代码是可以直接跑的,但显然会执行出错对不对,因为 123 和 "Hello" 并没有实现 run()方法

    ```main.py
    # 假设 A 和 B 是 class 并实现了 run() 方法
    l = [ A(), B(), 123, "Hello" ]

    for v in l:
    v.run()

    ```
    上面这问题虽然看起来一目了然,但在复杂的代码中是非常容易出现的(代码复杂了你没法保证会不会脑子抽了随便塞个奇怪的变量到上面的 list 里)

    那好,你一定就会想,有没有一套聪明办法,让我能限制 l 这个列表只能放实现了 `.run()` 方法的变量?比如像下面这样的伪代码,我自创了一个方法集 RunRunRun:

    ```
    # 定义一个方法集合
    method_set RunRunRun:
    run()


    l[ RunRunRun ] = [ A(), B(), 123, "Hello" ]

    # 我这里随便自创了一种写法,指定了列表 l 只能存放实现了方法集 RunRunRun 的变量
    # 因为 123 和 "Hello" 没有实现方法集 RunRunRun 里的所有方法,所以这行代码在编写阶段就可以被 IDE 检测出来,并且在编译阶段直接就能报错。而用不着每次到代码跑起来崩了才注意到
    ```

    在上面段伪代码中,方法集(method_set)的另一个通用的叫法就是接口(interface)。这就是接口的用处。这就是站在动态语言角度来理解接口,接口实际上就是在限制动态语言里过于自由的类型赋值。之所以上面很多人说得你不太理解,其实是站在静态语言角度在给你讲。如果你还不太理解的话,就像上面这样理解接口就行了。
    droppedbytech
        19
    droppedbytech  
       2022-04-11 16:14:51 +08:00
    我有点异端,我不认为 Go 的接口是 OOP 的设计。Go 的接口在我看来和其他 OOP 语言的最大区别是,你不需要声明你“实现”了一个接口,只要这个类有这个方法,它就是这个接口。这种方式我认为这是更“自然”,更符合软件开发特点的,用 Rob Pike (Go 创始人之一)的用语就是 organically 。你可以去看看 fmt.Fprintf 的使用就是一个典型例子,当你自己的类实现了一个 io.Writer 的接口,你就自然而然地可以使用这个方法,而整个过程你甚至不需要显式 import io 这个包。
    BrightSphere
        20
    BrightSphere  
       2022-04-11 16:18:15 +08:00 via Android
    这样就可以把依赖的 A 或者 B 替换成实现了接口 C 的 D 来进行单元测试了
    yunshangdetianya
        21
    yunshangdetianya  
    OP
       2022-04-11 16:37:17 +08:00
    @BeautifulSoap 非常感谢你的详细解答,我看了你的解答后好像明白点了,接口作用就是限制和约束类型
    yunshangdetianya
        22
    yunshangdetianya  
    OP
       2022-04-11 16:39:12 +08:00
    @BrightSphere 通用性
    FanGanXS
        23
    FanGanXS  
       2022-04-11 17:26:16 +08:00
    type notify interface {
    send()
    }

    // Email 实现 1
    type Email struct{}

    func (Email) send() {
    fmt.Println("email send")
    }

    // Qq 实现 2
    type Qq struct{}

    func (Qq) send() {
    fmt.Println("qq send")
    }

    func Test(n notify) {
    n.send()
    }

    假如说我现在需要实现一个通知的功能,但是我并不想具体采用哪种方式发通知(比如,微信、电子邮箱、QQ ),所以我就实现一个 notify 接口,它是通知的抽象(并没有具象到具体的采用哪放方式来实现通知)。接下来我利用 Emali 和 Qq 来对这个 notify 接口进行了实现,它们是这个抽象的具象。那么在外部调用的时候,我并不需要关心我具体使用哪种通知方式来发起通知,我只需要实现通知(就好像,老板通知你完成某一件事情,但是老板并不需要知道你完成某件事情的过程,采用哪种方式,他只想要这件事情的结果,而 Email 是同事 A 的完成方式,Qq 是同事 B 的完成方式,最后老板采用哪位同事来处理这件事情,是由老板自己决定,老板只需要指定某个同事就可以了)。即
    func main() {
    person.Test(&person.Qq{})
    }

    func main() {
    person.Test(&person.Email{})
    }
    konnnnn
        24
    konnnnn  
       2022-04-11 17:47:37 +08:00 via iPhone
    Python Oop => poop
    concernedz
        25
    concernedz  
       2022-04-11 17:51:26 +08:00
    @droppedbytech duck type
    raaaaaar
        26
    raaaaaar  
       2022-04-11 21:10:41 +08:00
    多态、面向抽象而不是实现、接口和实现的解耦等等,写个需要设计的轮子就懂了
    GeruzoniAnsasu
        27
    GeruzoniAnsasu  
       2022-04-11 23:13:03 +08:00
    @yunshangdetianya

    > 接口作用就是限制和约束类型

    正相反。

    静态类型语言的变量,类型是「唯一的」。你可能一下 get 不到有多唯一,我举个自己初学 oop 时对我帮助很大的例子

    - 你的程序有两个窗口
    - 窗口 A 叫 A ,B 叫<嘎>
    - 你给窗口 A 声明了一个类型 type WindowA struct{ Title: A }
    - 窗口 B 的类型 type WindowB struct{ Title: <嘎> }

    现在你想判断鼠标点击了哪个窗口:

    func findWhichClicked(x,y int) (T){}

    问题来了: 应该返回什么类型?返回 A ,那么点到 B 这个函数就给不出数据了,反之亦然。
    那……返回他们的公共类型?

    type WindowA{Window; Title }
    type WindowB{Window; Title }// AB 都来自 Window
    func findWhich(x,y int) (w Window){}

    但还是有问题: 我想获得点到的窗口的标题:

    w := findWhich()
    w.Title() // ?

    做不到!
    我永远只能获得 Window 的标题, 不能获得 WindowA 或者 WindowB 的标题。

    「这事简直跟我是男人了就不是人了一样荒唐」!!


    你可能会疑惑了,那用个公共变量呗

    if inA() {w=windowA}
    else if inB(){w=windowB} // 类型不匹配!,无法将 WindowA 类型的变量赋值给 Window 类型




    ------


    接口:
    type <有 title> interface {Title() }
    func findWhichClicked(x,y int) <有 title> {} // windowA 和 windowB 都可以实现 <有 title>
    w.Title() // w 可以是任何 <有 title>类型,运行期决定

    如果 w 是 windowA ,那么调用 windowA 的实现,是 B 调用 B 的实现。





    ------


    想想这句话: 「我要使用一个变量,它可能代表不同的东西,但类型又得是唯一的」
    tpkeeper
        28
    tpkeeper  
       2022-04-12 00:48:28 +08:00
    优点:
    1 后期维护方便,可以针对接口扩展实现不同的实例
    2 减少 package 之间的依赖,避免循环引用,大型项目中很有用
    afewok
        29
    afewok  
       2022-04-12 08:05:31 +08:00
    这不就是多态?
    tairan2006
        30
    tairan2006  
       2022-04-12 10:12:36 +08:00
    其实一般是为了单元测试 mock 才这么搞……如果不强调单元测试覆盖率,倒也不用处处这么玩。
    qq1009479218
        31
    qq1009479218  
       2022-04-12 10:19:17 +08:00
    你有两个结构体,一个是程序,叫 program ,另一个是人,叫 human ,他们都有个方法,.Run()

    有个接口叫 Runner ,他定义了一个方法叫做 .Run(),所以 program 和 human 都实现了这个接口

    现在有个函数叫做 代码太烂怎么办,WhatIfTheCodeBad(runner Runner),参数是 接口 Runner ,你传 program 和 human 都可以,因为他们都能跑 (实现了 Runner 接口)
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3741 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 37ms · UTC 00:15 · PVG 08:15 · LAX 16:15 · JFK 19:15
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.