接口(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
}
上面的示例代码中,咱们创立Book
和Phone
两个类型别离用于表明图书类与手机类产品,这两个类型具有了Goods
接口的办法,因而Book
和Phone
都完成了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
,如下图所示:
此刻接口类型变量与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
的值如下图所示:
假如对phone
进行初始化:
var phone *Phone = &Phone{Brand: "华为", Price: 6000, Num: 1, Discount: 0.8}
那么此刻goods
变量的动态类型和动态类型的值都不为nil
,如下图所示:
接口的嵌套
接口能够嵌套组合成为更杂乱的接口,比方咱们有以下两个接口:
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)
上面的代码中,w
是io.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-Switch
是switch
句子的一种特别用法,用于简化类型断语,其语法格式如下:
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))
}
接口运用主张
-
尽量运用规范库供给的接口,比方
error
,fmt.Stringer
等接口 -
接口内不要界说太多的办法,由于太多办法很难完成,咱们看到大多数规范库的接口都只要一个办法,比方Reader,error,Writer
-
只要当两个以上类型有一起的行为时,才将其行为笼统为接口,而不是在开发每个类型前就先写好接口
小结
接口是Go言语编程中比较重要的部分,无论是规范库仍是其他优秀开源库,处处都能够看到接口的运用。
好了,总结一下,在这篇文章中,咱们首要讲了以下几个点:
- 接口的界说与创立
- 怎么完成一个接口
- 接口值是什么
- 空接口的运用、类型断语与类型分支
- 运用接口的一点主张