在开始使用 Go 编程之前,我的大部分工作都是用 Python 完成的。作为一名 Python 程序员,我发现学习使用 Go 中的接口是非常困难的。基础很简单,而且我知道如何在标准库中使用接口,但是我做了很多练习之后才知道如何设计自己的接口。在本文中,我将讨论 Go 的类型系统,以解释如何有效地使用接口。

接口介绍

接口是什么?一个接口包含两层意思:它是一个方法的集合,同样是一个类型。让我们首先关注接口作为方法的集合这一方面。

通常,我们会用一些假设的例子来介绍接口。让我们来看看这个例子: Animal 类型是一个接口,我们将定义一个 Animal 作为任何可以说话的东西。这是 Go 类型系统的核心概念:我们根据类型可以执行的操作而不是其所能容纳的数据类型来设计抽象。

type Animal interface {

Speak() string

}

非常简单:我们定义 Animal 为任何具有 Speak 方法的类型。Speak 方法没有参数,返回一个字符串。所有定义了该方法的类型我们称它实现了 Animal 接口。Go 中没有 implements 关键字,判断一个类型是否实现了一个接口是完全是自动地。让我们创建几个实现这个接口的类型:

type Dog struct {

}

func (d Dog) Speak() string {

return "Woof!"

}

type Cat struct {

}

func (c Cat) Speak() string {

return "Meow!"

}

type Llama struct {

}

func (l Llama) Speak() string {

return "?????"

}

type JavaProgrammer struct {

}

func (j JavaProgrammer) Speak() string {

return "Design patterns!"

}

我们现在有四种不同类型的动物:Dog、Cat、Llama 和 JavaProgrammer。在我们的 main 函数中,我们创建了一个 []Animal{Dog{}, Cat{}, Llama{}, JavaProgrammer{}} ,看看每只动物都说了些什么:

func main() {

animals := []Animal{Dog{}, Cat{}, Llama{}, JavaProgrammer{}}

for _, animal := range animals {

fmt.Println(animal.Speak())

}

}

很好,现在你知道如何使用接口了,我不需要再讨论它们了,对吧?不是的。让我们来看看一些不太明显的东西。

interface{} 类型

interface{} 类型,空接口,是导致很多混淆的根源。interface{} 类型是没有方法的接口。由于没有 implements 关键字,所以所有类型都至少实现了 0 个方法,所以 所有类型都实现了空接口。这意味着,如果您编写一个函数以 interface{} 值作为参数,那么您可以为该函数提供任何值。例如:

func DoSomething(v interface{}) {

// ...

}

这里是让人困惑的地方:在 DoSomething 函数内部,v 的类型是什么?新手们会认为 v 是任意类型的,但这是错误的。v 不是任意类型,它是 interface{} 类型。对的,没错!当将值传递给DoSomething 函数时,Go 运行时将执行类型转换(如果需要),并将值转换为 interface{} 类型的值。所有值在运行时只有一个类型,而 v 的一个静态类型是 interface{} 。

这可能让您感到疑惑:好吧,如果发生了转换,到底是什么东西传入了函数作为 interface{} 的值呢?(具体到上例来说就是 []Animal 中存的是啥?)

一个接口值由两个字(32 位机器一个字是 32 bits,64 位机器一个字是 64 bits)组成;一个字用于指向该值底层类型的方法表,另一个字用于指向实际数据。我不想没完没了地谈论这个。如果您理解一个接口值是两个字,并且它包含指向底层数据的指针,那么这就足以避免常见的陷阱。如果您想了解更多关于接口实现的知识。这篇文章很有用:Russ Cox’s description of interfaces 。

在我们上面的例子中,当我们初始化变量 animals 时,我们不需要像这样 Animal(Dog{}) 来显示的转型,因为这是自动地。这些元素都是 Animal 类型,但是他们的底层类型却不相同。

为什么这很重要呢?理解接口是如何在内存中表示的,可以使得一些潜在的令人困惑的事情变得非常清楚。比如,像 “我可以将 []T 转换为 []interface{}

吗?” 这种问题就容易回答了。下面是一些烂代码的例子,它们代表了对 interface{} 类型的常见误解:

package main

import (

"fmt"

)

func PrintAll(vals []interface{}) {

for _, val := range vals {

fmt.Println(val)

}

}

func main() {

names := []string{"stanley", "david", "oscar"}

PrintAll(names)

}

运行这段代码你会得到如下错误:cannot use names (type []string) as type []interface {} in argument to PrintAll。如果想使其正常工作,我们必须将 []string 转为 []interface{}:

package main

import (

"fmt"

)

func PrintAll(vals []interface{}) {

for _, val := range vals {

fmt.Println(val)

}

}

func main() {

names := []string{"stanley", "david", "oscar"}

vals := make([]interface{}, len(names))

for i, v := range names {

vals[i] = v

}

PrintAll(vals)

}

很丑陋,但是生活就是这样,没有完美的事情。(事实上,这种情况不会经常发生,因为 []interface{} 并没有像你想象的那样有用)

指针和接口

接口的另一个微妙之处是接口定义没有规定一个实现者是否应该使用一个指针接收器或一个值接收器来实现接口。当给定一个接口值时,不能保证底层类型是否为指针。在前面的示例中,我们将方法定义在值接收者之上。让我们稍微改变一下,将 Cat 的 Speak() 方法改为指针接收器:

func (c *Cat) Speak() string {

return "Meow!"

}

运行上述代码,会得到如下错误:

cannot use Cat literal (type Cat) as type Animal in array or slice literal:

Cat does not implement Animal (Speak method has pointer receiver)

该错误的意思是:你尝试将 Cat 转为 Animal ,但是只有 *Cat 类型实现了该接口。你可以通过传入一个指针 (new(Cat) 或者 &Cat{})来修复这个错误。

animals := []Animal{Dog{}, new(Cat), Llama{}, JavaProgrammer{}}

让我们做一些相反的事情:我们传入一个 *Dog 指针,但是不改变 Dog 的 Speak() 方法:

animals := []Animal{new(Dog), new(Cat), Llama{}, JavaProgrammer{}}

这种方式可以正常工作,因为一个指针类型可以通过其相关的值类型来访问值类型的方法,但是反过来不行。即,一个 *Dog 类型的值可以使用定义在 Dog 类型上的 Speak() 方法,而 Cat 类型的值不能访问定义在 *Cat 类型上的方法。

这可能听起来很神秘,但当你记住以下内容时就清楚了:Go 中的所有东西都是按值传递的。每次调用函数时,传入的数据都会被复制。对于具有值接收者的方法,在调用该方法时将复制该值。例如下面的方法:

func (t T)MyMethod(s string) {

// ...

}

是 func(T, string) 类型的方法。方法接收器像其他参数一样通过值传递给函数。

因为所有的参数都是通过值传递的,这就可以解释为什么 *Cat 的方法不能被 Cat 类型的值调用了。任何一个 Cat 类型的值可能会有很多 *Cat 类型的指针指向它,如果我们尝试通过 Cat 类型的值来调用 *Cat 的方法,根本就不知道对应的是哪个指针。相反,如果 Dog 类型上有一个方法,通过 *Dog 来调用这个方法可以确切的找到该指针对应的 Gog 类型的值,从而调用上面的方法。运行时,Go 会自动帮我们做这些,所以我们不需要像 C语言中那样使用类似如下的语句 d->Speak() 。

例1:通过 Twitter API 获取正确的时间戳

Twitter API 使用下面的格式来展示时间戳:

"Thu May 31 00:00:01 +0000 2012"

Twitter API 返回的是一个 json 字符串,这里我们只考虑解析 created_at 字段:

package main

import (

"encoding/json"

"fmt"

"reflect"

)

// start with a string representation of our JSON data

var input = `

{

"created_at": "Thu May 31 00:00:01 +0000 2012"

}

`

func main() {

// our target will be of type map[string]interface{}, which is a

// pretty generic type that will give us a hashtable whose keys

// are strings, and whose values are of type interface{}

var val map[string]interface{}

if err := json.Unmarshal([]byte(input), &val); err != nil {

panic(err)

}

fmt.Println(val)

for k, v := range val {

fmt.Println(k, reflect.TypeOf(v))

}

}

运行上述代码,输出:

map[created_at:Thu May 31 00:00:01 +0000 2012]

created_at Thu May 31 00:00:01 +0000 2012 string

我们得到了解析后的结果,但是解析出来的时间是字符串类型的,作用有限,因此我们想把它解析成 time.Time 类型的,对代码做出如下修改:

var val map[string]interface{} -> var val map[string]time.Time

结果出错了:

panic: parsing time ""Thu May 31 00:00:01 +0000 2012"" as ""2006-01-02T15:04:05Z07:00"": cannot parse "Thu May 31 00:00:01 +0000 2012"" as "2006"

出错的原因是字符串格式与 Go 中的时间格式不匹配(因为 Twitter's API 是用 Ruby 写的,其格式跟 Go 不同)。我们必须定义我们自己的类型来解析时间。encoding/json 在解析时会判断传入 json.Unmarshal 的值是否实现了 json.Unmarshaler 接口:

type Unmarshaler interface {

UnmarshalJSON([]byte) error

}

如果实现了,就会调用 UnmarshalJSON 方法来解析(参考),所以我们需要的是一个实现了 UnmarshalJSON([]byte) error 方法的类型:

type Timestamp time.Time

func (t *Timestamp) UnmarshalJSON(b []byte) error {

// ...

}

值得注意的是,我们使用一个指针作为方法接受者,因为我们希望在方法内对接受者进行更改。UnmarshalJSON 中,t 代表指向 Timestamp 类型值的指针,通过 *t 我们可以访问到这个值,这样就可以修改它了。

我们可以使用 time.Parse(layout, value string) (Time, error) 来解析时间,该函数的第一个参数是表示时间格式的字符串(更多字符串格式),第二个是我们要解析的字符串。返回 time.Time 类型的值以及 error(如果解析出错)。解析得到 time.Time 类型的值后,转换成 Timestamp 类型然后赋值给 *t:

func (t *Timestamp) UnmarshalJSON(b []byte) error {

v, err := time.Parse(time.RubyDate, string(b[1:len(b)-1]))

if err != nil {

return err

}

*t = Timestamp(v)

return nil

}

注意,传入函数的 []byte 是原始的 JSON 数据,其中包含有引号,所以这里需要切片去掉引号。

例2:从 HTTP 请求中得到对象

然我们设计一个接口来解决 web 开发中常见的一个问题:我们想解析 HTTP 请求体得到我们需要的对象数据。例如,我们这样定义我们的接口:

GetEntity(*http.Request) (interface{}, error)

因为 interface{} 可以有任意的底层类型,所以我们可以解析得到任何我们需要的东西。但是这是一个不好的设计,我们将过多的逻辑引入到 GetEntity 函数中,GetEntity 函数现在需要针对每一种新类型进行修改,我们需要使用类型断言来处理返回的值。在实践中,返回 interface{} 的函数往往很烦人,作为一个经验法则,您只需要记住,将 interface{} 作为参数而不是返interface{} 值通常更好(Postel’s Law)。

我们也可能会尝试编写一些返回类型明确的函数,像这样:

GetUser(*http.Request) (User, error)

但是这样又显得不够灵活,因为需要对不同的类型写不同的函数。我们真正需要的是像这样的一个设计:

type Entity interface {

UnmarshalerHTTP(*http.Request) error

}

func GetEntity(r *http.Request, v Entity) error {

return v.UnmarshalerHTTP(r)

}

GetEntiry 方法需要传入一个参数,该参数为 Entity 接口类型,确保实现了 UnmarshalHTTP 方法。为了使用该方法,我们需要定义 User 类型并实现 UnmarshalHTTP 方法,并在方法中解析 HTTP 请求:

type User struct {

...

}

func (u *User) UnmarshalHTTP(r *http.Request) error {

// ...

}

然后,定义一个 User 类型的变量,并将其指针传递给 GetEntity 方法:

var u User

if err := GetEntity(req, &u); err != nil {

// ...

}

这同解析 JSON 数据类似。这种方式可以始终如一地安全地工作,因为 var u User 将自动地将 User 结构体初始化为零值。Go 不像其他语言一样声明和初始化是分开进行的。通过声明一个值而不初始化它,运行时将为该值分配适当的内存空间。即使我们的 UnmarshalHTTP 方法不能使用某些字段,这些字段也将包含有效的零数据,而不是垃圾数据。

结语

我希望读完此文后你可以更加得心应手地使用 Go 中的接口,记住下面这些结论:

通过考虑数据类型之间的相同功能来创建抽象,而不是相同字段

interface{} 的值不是任意类型,而是 interface{} 类型

接口包含两个字的大小,类似于 (type, value)

函数可以接受 interface{} 作为参数,但最好不要返回 interface{}

指针类型可以调用其所指向的值的方法,反过来不可以

函数中的参数甚至接受者都是通过值传递

一个接口的值就是就是接口而已,跟指针没什么关系

如果你想在方法中修改指针所指向的值,使用 * 操作符

go语言如何调用java接口,[译] 如何在 Go 中使用接口相关推荐

  1. R语言中导入java中_R语言直接调用Java代码

    我们都知道Java语言长期处于霸主地位,在所有编程语言排行榜中,Java也是常年位居第一.在近几年也是爆炸式发展,几乎覆盖到了应用开发的所有领域.而R语言则在统计圈和数据可视化处于佼佼者,如果将Jav ...

  2. java定义接口必用关键字_Java中定义接口的关键字是什么

    Java中定义接口的关键字是什么 Java中定义接口的关键字是"interface"."interface"是面向对象编程语言中接口操作的关键字,功能是把所需成 ...

  3. java接口有什么用_Java 中的接口有什么作用?好处?

    接口的作用就是把使用接口的人和实现接口的人分开,实现接口的人不必要关心谁去使用,而使用接口的人也不用关心谁实现的接口,由接口将他们联系在一起. 很多JAVA初级程序员对于接口存在的意义很疑惑.不知道接 ...

  4. java spring scope_如何在Spring中自定义scope的方法示例

    大家对于 Spring 的 scope 应该都不会默认.所谓 scope,字面理解就是"作用域"."范围",如果一个 bean 的 scope 配置为 sing ...

  5. golang 切片 接口_如何理解Golang中的接口?

    个人认为,要理解 Go 的接口,一定先了解下鸭子模型. 鸭子模型 那什么鸭子模型? 鸭子模型的解释,通常会用了一个非常有趣的例子,一个东西究竟是不是鸭子,取决于它的能力.游泳起来像鸭子.叫起来也像鸭子 ...

  6. matlab verilog 接口,使用SystemVerilog简化FPGA中的接口

    FPGA工程师们应该都会吐槽Verilog的语法,相当的不友好,尤其是对于有很多接口的模块,像AXI4/AXI-Lite这种常用的总线接口,动不动就好几十根线,写起来是相当费劲. 当然现在Xilinx ...

  7. java发送gmail_如何在Gmail中轻松通过电子邮件发送人群

    java发送gmail Mailing lists are an old tool in the email arsenal, but their implementation in Gmail is ...

  8. cmd中加载java源文件_如何在cmd中编译和运行java源文件

    如何在cmd中编译和运行java源文件 首先写一个名为HelloWorld.java的java源文件,存储在如C:/java/src的地址,我们再假设待会要存储的位置是C:/java/bin,则我们做 ...

  9. go语言如何调用java接口_Go语言实现的Java Stream API

    学习Go语言时实现的集合操作工具库,类似于Java 8 中新增的Stream API.由于Go语言不支持泛型,所以基于反射实现.只用于学习目的,不要用于生产(PS:当然也不会有人用). 集合操作包括生 ...

最新文章

  1. 触发ALV事件时报错MOVE_TO_LIT_NOTALLOWED_NODATA
  2. [算法学习]斐波那契数的计算
  3. reactjs Context的使用:一种组件间的通信方式
  4. PHP限制提交文章条数,限制WordPress文章页面修订版本(revision)数量
  5. grace hopper_开源日,在Grace Hopper会议上建立FOSS技能
  6. axure小程序模板_公众号和小程序模板消息
  7. 43.django中form组件
  8. IP电话的VoIP语音网关设计
  9. 茶几 清明上河图 诗句
  10. apng2gif各种问题的解决
  11. 缓解眼睛疲劳,护眼调节色温软件推荐
  12. chrome浏览器下载速度慢问题解决办法
  13. 网页复制内容修改(解决复制文本多空格问题,带标注信息(如版权信息))
  14. IT招聘惨淡季?求职者无offer,招聘者无简历
  15. 基于服务器端的三维渲染技术
  16. (四十三):Comprehensive Linguistic-Visual Composition Network for Image Retrieval
  17. vue3+ts+el-tabs+keep-alive
  18. 【转】架构师成长历程:行到水穷处,坐看云起时
  19. 《安富莱嵌入式周报》第309期:开源脑机接口套件,三相多电平逆变器,开源电位仪,单片机并行计算,蓝牙市场分析报告,AI密码破解,nRF54H2发布,Z-Wave
  20. Xshell的安装和使用

热门文章

  1. 【干货】趣头条基于 Flink+ClickHouse 构建实时数据分析平台
  2. 缓存击穿、缓存失效及热点key的解决方案
  3. 常用 AT 命令手册
  4. jQuery 中的事件参数传递机制
  5. 运行连接远程服务器失败,应用技巧:如何应付远程控制服务器失败
  6. 在机器学习中为什么只需要正的误差而不用负的误差
  7. leetcode —— 17. 电话号码的字母组合
  8. leetcode —— 面试题 17.08. 马戏团人塔
  9. SH760模态分析-多种解析与数字计算方法
  10. 加加速度matlab,关于加加速度的若干机械运动分析及MATLAB模拟