接口(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言语编程中比较重要的部分,无论是规范库仍是其他优秀开源库,处处都能够看到接口的运用。

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

  • 接口的界说与创立
  • 怎么完成一个接口
  • 接口值是什么
  • 空接口的运用、类型断语与类型分支
  • 运用接口的一点主张