likes
comments
collection
share

重学Go语言 | Go接口详解

作者站长头像
站长
· 阅读数 16

接口(interface)与类(class)一样,都是面向对象编程的重要组成部分,Go语言中虽然没有类的概念,不过Go支持接口。

接口

Go语言的接口与其他编程语言的接口有什么不同呢?下面我们来探究一下!

定义

什么是接口?简单来说接口就是一个方法集,这个方法集描述了其他数据类型实现该接口的方法,接口内只能定义方法,且这些方法没有具体地实现,也就说这些方法只有方法名与方法签名而已,没有方法体。

创建

Go接口创建与创建结构体的类似,创建接口使用interface关键字:

type Goods interface {
	GetPrice() float64
	GetNum() int
}

创建了接口后,就可以使用该接口类型定义变量了:

var goods Goods
log.Println(goods) //nil

接口类型变量的默认值是nil,直接调用的话会报错:

goods.GetPrice() //panic

接口的实现

在其他编程语言中(比如在Java),如果我们要实现一个接口,要在类名后使用implements指定所要实现的接口,并且需要实现接口中的每一个方法:

public class Book implements Goods{
  
    public int GetNum(){
        //...
    }
  
    public float GetPrice(){
        //..
    }
}

Go接口采用的是隐式实现,也就说在Go语言中,要实现某个接口不用指定所要实现的接口,只要该类型拥有拥有某个接口的所有方法时,我们就说该类型就实现了这个接口:

type Book struct {
	Name  string
	Price float64
	Num   int
}

func (b *Book) GetPrice() float64 {
	return b.Price
}

func (b *Book) GetNum() int {
	return b.Num
}

type Phone struct {
	Brand    string
	Discount float64
	Price    float64
	Num      int
}

func (p *Phone) GetPrice() float64 {
	return p.Price * p.Discount
}

func (p *Phone) GetNum() int {
	return p.Num
}

上面的示例代码中,我们创建BookPhone两个类型分别用于表示图书类与手机类商品,这两个类型拥有了Goods接口的方法,因此BookPhone都实现了Goods接口。

将实现了该接口的类型实例赋给接口类型变量,这时候再调用方法,就不会报错了:

var goods Goods = Phone{Brand: "华为", Price: 6000, Num: 1, Discount: 0.8}
fmt.Println(goods.GetPrice())

接口的好处

使用接口的好处在于,对于某类有共同行为的数据类型,可以通过接口描述其共同行为,但不同类型的行为可以有不同的实现逻辑,下面我们通过一个示例来讲解一下:

package main

import "fmt"

type Cart struct {
	Goods []Goods
}

func (c *Cart) Add(g Goods) {
	c.Goods = append(c.Goods, g)
}

func (c *Cart) TotalPrice() float64 {
	//计算购物车物品价格...
	var totalPrice float64
	for _, g := range c.Goods {
		totalPrice += float64(g.GetNum()) * g.GetPrice()
	}
	return totalPrice
}

func main() {
  //图书类商品
	b1 := Book{Name: "Go从入门到精通", Price: 50, Num: 2}
	b2 := Book{Name: "Go Action", Price: 60, Num: 1}
	//手机
	p1 := Phone{Brand: "华为", Price: 6000, Num: 1, Discount: 0.8}

	c := &Cart{}
	c.Add(&b1) //添加到购物车
	c.Add(&b2)
	c.Add(&p1)
	fmt.Println(c.TotalPrice())//计算价格
}

在上面的例子中,我们希望计算购物车内商品的总价格,虽然商品是多种多样的,但计算总价格的逻辑里只关心商品的价格和数量,因为将其抽象为Goods接口,只有实现了该接口的商品才允许被添加到购物车中,在计算购物车价格时,也不用管不同商品的价格计算逻辑,只需要获得该商品的价格与数量。

接口值

前面我们说过,接口变量的默认值是nil,实际上这是把整个接口类型的变量当作一个整体,再细究起来,一个接口类型变量由两个部分组成:一个具体的类型和该类型的值,当一个接口类型变量的值为nil时,表示其动态类型和动态类型的值都是nil,如下图所示:

重学Go语言 | Go接口详解

此时接口类型变量与nil进行比较是相等的:

var goods Goods

if goods == nil{
  fmt.Println("goods equal nil")
}

接下来,我们看看下面这段代码:

var phone *Phone
var goods Goods

goods = phone

if goods == nil{
  fmt.Println("goods equal nil")
}else{
  fmt.Println("goods don't equal nil")
}

fmt.Println(goods.GetNum()) //调用方法

这段代码的运行结果:

goods don't equal nil
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x20 pc=0x1089780]

这时候你可能会觉得奇怪,goods并不等于nil,为什么调用里面的方法会引发panic呢?

其实上面代码中将phone赋给goods后,由于phone类型为nil,因此这个时候goods的动态类型为phone,但是该类型的值为nil,goods的值如下图所示:

重学Go语言 | Go接口详解

如果对phone进行初始化:

var phone *Phone = &Phone{Brand: "华为", Price: 6000, Num: 1, Discount: 0.8}

那么此时goods变量的动态类型和动态类型的值都不为nil,如下图所示:

重学Go语言 | Go接口详解

接口的嵌套

接口可以嵌套组合成为更复杂的接口,比如我们有以下两个接口:

type Reader interface {
	Read(p []byte) (n int, err error)
}

type Writer interface {
	Write(p []byte) (n int, err error)
}

如果我们拥有一个同时拥有Read()Write()方法的接口,可能会这样做:

type ReaderWriter interface {
  Read(p []byte) (n int, err error)
  Write(p []byte) (n int, err error)
}

实际上更好的做法是将简单的接口组合为更复杂的接口,达到代码复用的效果:

type ReadWriter interface {
    Reader
    Writer
}

嵌套的同时,也可以继续增加方法:

type File interface {
    Reader
    Writer
    Close()
}

空接口

如同用struct{}来表示一个空结构体一样,interface{}表示一个空接口,空接口是最简单的接口,任何类型都默认实现了空接口,这意味着可以把任何类型赋予一个空接口变量:

package main

func PrintAny(args ...interface{}) {
	//打印...
}

func main() {
	//将字符串赋给空接口类型的变量
	s1 := "s2"
	i1 := 10
	PrintAny(s1, i1)

	//将整数赋给空接口类型的变量
	i2 := 10
	PrintAny(i2)
}

上面代码中,我们自己定义了一个打印函数,该函数可以接收一个或多个空接口类型的参数,实际上,Go的fmt标准包提供的打印函数都是这么做的:

func Println(a ...any) (n int, err error) {
	return Fprintln(os.Stdout, a...)
}

fmt.Println()打印函数的any就是空接口的一个别名,其定义如下:

type any = interface{}

类型断言

一个接口可以有多种不同的实现,当我们想判断某个接口类型的变量的动态类型是哪种类型时,也称为类型断言,其表达式语法如下:

x.(T)

该表达式也有返回值:

v := x.(T)

x.(T)断方要分两种情况来看:

第一种情况x是一个接口类型变量,T是一个具体类型变量,用于判断某个接口变量当前的具体类型。

比如我们想判断当某个Goods类型的变量是不是Book时:

book := Book{Name: "Go从入门到精通", Price: 50, Num: 2}
var goods Goods = &book

v := goods.(*Book)
fmt.Println(v.GetNum())

断言的返回值v在断言成功后,就获得了断言类型对象的值,比如上面的例子中,

if v == b {
	fmt.Println("v==b")	
}

如果断言类型错误,会引发panic的错误:

goods.(*Phone)//panic

x.(T)表达式的第二个返回值可以来判断断言是否成功,这样就不会引发panic

if v,ok := goodf.(*Phone);ok{

} 

第二种情况是x仍然是一个接口的变量,T是另外一个接口类型变量,用于判断某个类型是否实现了另外一个接口:

var w io.Writer
w = os.Stdout //io.File
rw := w.(io.ReadWriter) 

fmt.Println(rw)

上面的代码中,wio.Writer接口变量,因此只包含该接口的方法,后面执行rw := w.(io.ReadWriter) 后,其返回rw就拥有了io.ReadWriter接口的方法。

类型分支

很多情况下,我们要进行类型断言时,经常会这么做:

var x interface{}
x = 1
if x == nil {
    return "nil"
} else if _, ok := x.(int); ok {
    return fmt.Sprintf("%d", x)
} else if _, ok := x.(uint); ok {
    return fmt.Sprintf("%d", x)
} else if b, ok := x.(bool); ok {
    if b {
        return "TRUE"
    }
    return "FALSE"
} else if s, ok := x.(string); ok {
    return sqlQuoteString(s) 
} else {
    panic(fmt.Sprintf("unexpected type %T: %v", x, x))
}

实际上Go支持更简洁的写法,就是用type-switch语句。

type-Switchswitch语句的一种特殊用法,用于简化类型断言,其语法格式如下:

switch x.(type) {

  case nil:       
  
  ...
  
  default:        
}

因此上面的类型断言的例子代码可以改为:

var x interface{}
x = 1
switch x := x.(type) {
  case nil:      
  	return "nil"
  case int, uint: 
  	return fmt.Sprintf("%d", x)
  case bool: 
  	if b {
        return "TRUE"
    }
    return "FALSE"
  case string:   
  	 return sqlQuoteString(x) 
  default:  
    panic(fmt.Sprintf("unexpected type %T: %v", x, x))
}

接口使用建议

  • 尽量使用标准库提供的接口,比如errorfmt.Stringer等接口

  • 接口内不要定义太多的方法,因为太多方法很难实现,我们看到大多数标准库的接口都只有一个方法,比如Reader,error,Writer

  • 只有当两个以上类型有共同的行为时,才将其行为抽象为接口,而不是在开发每个类型前就先写好接口

小结

接口是Go语言编程中比较重要的部分,无论是标准库还是其他优秀开源库,处处都可以看到接口的使用。

好了,总结一下,在这篇文章中,我们主要讲了以下几个点:

  • 接口的定义与创建
  • 如何实现一个接口
  • 接口值是什么
  • 空接口的使用、类型断言与类型分支
  • 使用接口的一点建议
转载自:https://juejin.cn/post/7250009145914818620
评论
请登录