【go】gopl学习笔记(4.接口)

本文详细介绍了Go语言中的接口,包括接口类型、接口组合、实现接口的方式以及接口值。探讨了接口在类型抽象、类型断言和类型分支中的应用,并提供了设计接口的实用建议。通过例子展示了如何利用接口实现多态和错误处理,强调了接口在面向对象编程中的重要性。
摘要由CSDN通过智能技术生成

前言

我们都知道OOP有两个方式,组合继承组织类,分别表示has-a关系和is-a关系,能够实现代码复用、提升扩展性和灵活性。

前面一篇讲方法method阐述了go通过结构体struct嵌套实现对象的扩展(相对于Java类的组合,has a关系),outer结构体会自动生成包装方法委托给inner嵌套结构的方法,这样实现了方法代码的复用。这样有了组合has-a关系,go里面的is-a关系是怎么实现的呢?下面我们来看看接口interface~

1.接口类型

类似于Java中的接口,接口类型描述了一系列方法的集合,一个实现了这些方法的具体类型是这个接口类型的实例。实现接口则不同于Java中需要显式地指明具体类implements实现了某个接口,go中的实现是隐式的,只要一个具体类型实现了某个接口的所有方法就足够了。

以我们最常用的fmt包的打印部分为例,PrintfSprintf都包含格式化输出的部分,只是最终输出的目的地不一样,一个是标准输出还有一个是字符串,所以fmt包的实现将通用的格式化输出的部分抽取到Fprintf,然后将通用逻辑委托给它。Fprintf的前缀F表示文件(File),输出结果应该被写入第一个参数,第一个参数io.Writer是是用得最广泛的接口之一,因为它提供了所有类型的写入bytes的抽象,包括文件类型,内存缓冲区,网络链接等等。Reader则相反,代表任意可以读取bytes的类型。

func Printf(format string, args ...interface{}) (int, error) {
    return Fprintf(os.Stdout, format, args...) // 通用逻辑
}
func Sprintf(format string, args ...interface{}) string {
    var buf bytes.Buffer
    Fprintf(&buf, format, args...) // 通用逻辑
    return buf.String()
}
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {}

// 接口
type Writer interface {
	Write(p []byte) (n int, err error) 
}
type Stringer interface {
    String() string
}

1.2 接口组合(嵌套)

新的接口类型通过组合已有的接口来定义,这样可以很简短地包括所有方法,也可以与声明方法混合使用

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

1.3 实现接口

具体类型只要实现了接口的所有方法,就是实现了接口,该具体类型的实例就可以赋值给该接口的引用。

var rwc io.ReadWriteCloser
rwc = os.Stdout // OK: *os.File 有 Read, Write, Close 方法

var w io.Writer
w = rwc // OK: io.ReadWriteCloser有Write 赋值给超类型 向上隐式转换
rwc = w // 编译报错: io.Writer没有Close方法 赋值给子类型 向下error

回想方法method讲到类型的方法有两种形式的接收者(t T) 或者 (t *T),那两种方式都可以吗?还有就是包级函数(没有接收者)可以吗?

两种类型都可以,但是它们是作为两种类型分别实现了相关方法。如何选择呢?大多数是指针类型*T,特别是像Write方法那样隐式的给接收者带来变化的时候。

type IntSet struct { /* ... */ }
func (*IntSet) String() string

var s IntSet
var _ fmt.Stringer = &s  // OK *IntSet类型实现了Stringer接口
var _ fmt.Stringer = s  // 编译报错: IntSet没有实现Stringer接口

用接口类型的变量引用具体类型的实例,可以打到封装和隐藏的作用,隐藏了除了接口以外的方法和字段。

关于interface{}类型,它没有任何方法,请讲出哪些具体的类型实现了它?interface{}被称为空接口类型是不可或缺的。因为空接口类型对实现它的类型没有要求,类似于Java的Object,作为一个超类出现,所以我们可以将任意一个值赋给空接口类型。区别在于Java中的基础类型赋值为Object需要隐式包装类处理,而Go不需要。

var any interface{}
any = true
any = 12.34
any = "hello"
any = map[string]int{"one": 1}
any = new(bytes.Buffer)

但是实例赋值给interface{}变量,就不能直接对其值做任何操作,因为空接口没有任何方法,可以通过类型断言获取其方法。

var _ io.Writer = (*bytes.Buffer)(nil)  // 只是告诉 右边的类型实现了左边的接口

1.4 接口抽象

在设计对象和类型时,将类型的通用行为抽取出来,放入接口;

如果多个接口之间还有通用方法,可以继续抽象父接口,然后通过接口组合的方式,

最终形成类似Java中的继承树。

2.接口值

接口值,包括一个具体的类型和那个类型的值,即动态类型和动态值(类似java的多态,有着更细致的子类和子类的实例)。一个接口值类似于Java的一个对象,对象头中包含类型指针,对象体包含各个字段。

如下例

var x interface{} = time.Now()

x变量在内存中包含一个type对应于java对象头中的类型指针,value是实例字段部分,Time是个结构体,包含3个字段如下:。

调用x的方式时,其实时(x.value).(type.method)(),动态类型type决定方法,value决定方法的接收者。

其中type标识的是具体类型,而不是接口类型,如下将os.Stout *os.File赋值给io.Writer接口变量,如图7.2所属type字段是具体类型*os.File。

var w io.Writer
w == nil
w.Write([]byte("hello")) // panic: nil pointer dereference 类似于Java的NPE

w = os.Stdout

  • 接口值为空时,不能调用起任何方法,记得判断。
  • 接口调用是动态分配(多态,重写),获取实际type的对应方法地址间接调用,recevier就是value存储实例。

接口值可以使用==和!=来进行比较,不像Java需要用equals方法比较。但是如果动态类型不可比较的(比如Slice),将它们进行比较就会失败并且panic!!所以通常需要获取接口值的动态类型,通常使用fmt包的%T动作如下,fmt是通过反射获取接口动态类型的名称。

var w io.Writer
fmt.Printf("%T\n", w) // "<nil>"
w = os.Stdout
fmt.Printf("%T\n", w) // "*os.File"
w = new(bytes.Buffer)
fmt.Printf("%T\n", w) // "*bytes.Buffer"
  • 接口值非空,valuenil时调用方法NPE
func main() {
	var buf *bytes.Buffer  // 改为 var buf io.Writer 则接口值为nil,check能拦住
	f(buf) // NOTE: subtly incorrect!

}
func f(out io.Writer) { //隐式转换,buf从*bytes.Buffer转换成Writer,接口值不为nil,但接口值的value字段为nil
	if out != nil {
		out.Write([]byte("done!\n"))
	}
}

3.类库常用接口

接口功能
flag.Value实现了可以从命令行参数接收-name value解析特定值
sort.Interface

类似Java的Comparator将比较逻辑传递个特定排序算法

type Interface interface {
    Len() int
    Less(i, j int) bool // i, j are indices of sequence elements
    Swap(i, j int)
}


type StringSlice []string
func (p StringSlice) Len() int           { return len(p) }
func (p StringSlice) Less(i, j int) bool { return p[i] < p[j] }
func (p StringSlice) Swap(i, j int)      { p[i], p[j] = p[j], p[i] }

sort.Sort(StringSlice(names))

4.类型断言

类型断言是一个作用在在接口值(第二节)上的操作,语法上x.(T)被称为断言类型,x指接口值、T表示一个类型,作用就是判断接口值x(对象)是否是T类型。有两种可能,

  • (1)当T是具体类型时,就是判断x的type字段是否是T;如果判断成功,返回动态类型T的对象,否则抛出异常panic。
var w io.Writer
w = os.Stdout
f := w.(*os.File) // success: f == os.Stdout
c := w.(*bytes.Buffer) // panic: interface holds *os.File, not *bytes.Buffer

f, ok := w.(*os.File)      // 成功flag success:  ok, f == os.Stdout
  • (2)T是接口类型,判断x的type类型是否实现了T;判断成功,返回T类型新的接口值。

nil接口值,不论被断言类型T是什么,都会失败。

类型断言主要就是用于检验它是否是一些特定的类型,向下转型(父类型转换成子类型),类似于Java的强制转换Son son = (Son) father

4.1 应用场景

4.1.1 识别错误类型

在错误处理的时候经常要判断是哪一种类型的错误,然后才好做不同的处理。以下是os包中文件相关的报错,如何判断是文件不存在的呢?

_, err := os.Open("/no/such/file")
fmt.Println(os.IsNotExist(err)) // "true"

var ErrNotExist = errors.New("file does not exist")

// 返回是否文件不存在 
func IsNotExist(err error) bool {
    if pe, ok := err.(*PathError); ok {  // 类型断言
        err = pe.Err    // 获取其包装内的error值
    }
    return err == syscall.ENOENT || err == ErrNotExist
}

4.1.2 writeString优化

func WriteString(w Writer, s string) (n int, err error) {
	if sw, ok := w.(StringWriter); ok {    // 判断是否可以直接写String
		return sw.WriteString(s) // 免于转换
	}
	return w.Write([]byte(s)) // 字符串转切片,申请内存并拷贝,耗时耗空
}

5.类型分支

通常一个接口值,不同的类型有着不同的处理逻辑,需要判断其类型,go提供了一种类型分支,

如下,switch一个接口值的类型上,在每个单一类型的case内部,变量x和这个case的类型相同。

switch x := x.(type) { /* ... */ }     // x.(type)语法只能用在类型分支中,判断类型

func sqlQuote(x interface{}) string {
    switch x := x.(type) {
    case nil:
        return "NULL"
    case int, uint:
        return fmt.Sprintf("%d", x) // x has type interface{} here.
    case bool:
        if x {
            return "TRUE"
        }
        return "FALSE"
    case string:
        return sqlQuoteString(x) // (not shown)
    default:
        panic(fmt.Sprintf("unexpected type %T: %v", x, x))
    }
}

在类型分支中,x虽是interface{},其实它是可识别联合(一种隐含约束),满足这个联合是包含nil、int、unit、bool和string类型的集合。

满足可识别联合的具体类型的集合被设计为确定和暴露,而不是隐藏。可识别联合的类型几乎没有方法,操作它们的函数使用一个类型分支的case集合来进行表述,这个case集合中每一个case都有不同的逻辑。

设计Tips

  1. 不为了面向接口而为仅一个实现设计接口,避免不必要抽象,仅多种类型有相同行为时需抽象出接口。
  2. 为了扩展,(跨包扩展)可以通过接口,即接口export,但是实现type不导出,两个包有不同的实现,
  3. ask only for what you need,向下转型时最小接口原则。

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值