接口
接口类型是对其它类型行为的抽象和概括;因为接口类型不会和特定的实现细节绑定在一起,通过这种抽象的方式我们可以让我们的函数更加灵活和更具有适应能力。
但Go语言中接口类型的独特之处在于它是满足隐式实现的。也就是说,我们没有必要对于给定的具体类型定义所有满足的接口类型;简单地拥有一些必需的方法就足够了。这种设计可以让你创建一个新的接口类型满足已经存在的具体类型却不会去改变这些类型的定义;当我们使用的类型来自于不受我们控制的包时这种设计尤其有用。
约定
具体类型+接口类型(接口类型是一种抽象的类型。它不会暴露出它所代表的对象的内部值的结构和这个对象支持的基础操作的集合;它们只会表现出它们自己的方法)。
它不会暴露出它所代表的对象的内部值的结构和这个对象支持的基础操作的集合;它们只会表现出它们自己的方法。也就是说当你有看到一个接口类型的值时,你不知道它是什么,唯一知道的就是可以通过它的方法来做什么。
package fmt
func Fprintf(w io.Writer, format string, args ...interface{}) (int, error)
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()
}
即使Fprintf函数中的第一个参数也不是一个文件类型。它是io.Writer类型,这是一个接口类型定义如下:
package io
type Writer interface {
Write(p []byte) (n int, err error)
}
类型
接口类型具体描述了一系列方法的集合,一个实现了这些方法的具体类型是这个接口类型的实例。
io.Writer类型是用得最广泛的接口之一,因为它提供了所有类型的写入bytes的抽象,包括文件类型,内存缓冲区,网络链接,HTTP客户端,压缩工具,哈希等等。Reader可以代表任意可以读取bytes的类型,Closer可以是任意可以关闭的值。
我们发现有些新的接口类型通过组合已有的接口来定义。下面是两个例子:
type ReadWriter interface {
Reader
Writer
}
type ReadWriteCloser interface {
Reader
Writer
Closer
}
上面用到的语法和结构内嵌相似,我们可以用这种方式以一个简写命名一个接口,而不用声明它所有的方法。这种方式称为接口内嵌。尽管略失简洁,我们可以像下面这样,不使用内嵌来声明io.ReadWriter接口。
type ReadWriter interface {
Read(p []byte) (n int, err error)
Write(p []byte) (n int, err error)
}
或者甚至使用一种混合的风格:
type ReadWriter interface {
Read(p []byte) (n int, err error)
Writer
}
实现接口的条件
一个类型如果拥有一个接口需要的所有方法,那么这个类型就实现了这个接口。
接口指定的规则非常简单:表达一个类型属于某个接口只要这个类型实现这个接口。所以:
var w io.Writer
w = os.Stdout // OK: *os.File has Write method
w = new(bytes.Buffer) // OK: *bytes.Buffer has Write method
w = time.Second // compile error: time.Duration lacks Write method
T类型的值不拥有所有*T
指针的方法,这样它就可能只实现了更少的接口。
IntSet
类型的String方法的接收者是一个指针类型,所以我们不能在一个不能寻址的IntSet
值上调用这个方法:
type IntSet struct { /* ... */ }
func (*IntSet) String() string
var _ = IntSet{}.String() // compile error: String requires *IntSet receiver
但是我们可以在一个IntSet变量上调用这个方法:
var s IntSet
var _ = s.String() // OK: s is a variable and &s has a String method
然而,由于只有*IntSet
类型有String方法,所以也只有*IntSet
类型实现了fmt.Stringer接口:
var _ fmt.Stringer = &s // OK
var _ fmt.Stringer = s // compile error: IntSet lacks String method
interface{}被称为空接口类型是不可或缺的。因为空接口类型对实现它的类型没有要求,所以我们可以将任意一个值赋给空接口类型。
一个具体的类型可能实现了很多不相关的接口。
不像基于类的语言,他们一个类实现的接口集合需要进行显式的定义,在Go语言中我们可以在需要的时候定义一个新的抽象或者特定特点的组,而不需要修改具体类型的定义。当具体的类型来自不同的作者时这种方式会特别有用。
flag.Value
接口
为我们自己的数据类型定义新的标记符号是简单容易的。我们只需要定义一个实现flag.Value
接口的类型。
(所谓标记就是类似于在命令行中执行:./sleep -period 50ms
中的-period
一样)
package flag
// Value is the interface to the value stored in a flag.
type Value interface {
String() string
Set(string) error
}
String方法格式化标记的值用在命令行帮助消息中;这样每一个flag.Value
也是一个fmt.Stringer
。Set方法解析它的字符串参数并且更新标记变量的值。
type celsiusFlag struct {
tempconv.Celsius
}
func (f *celsiusFlag) String() string {
return fmt.Sprintf("%gC", f.Celsius)
}
func (f *celsiusFlag) Set(s string) error {
var unit string
var value float64
fmt.Sscanf(s, "%f%s", &value, &unit)
switch unit {
case "C", "°C":
f.Celsius = tempconv.Celsius(value)
return nil
case "F":
f.Celsius = tempconv.FToC(tempconv.Fahrenheit(value))
return nil
}
return fmt.Errorf("invalid temp %q", s)
}
func CelsiusFlag(name string, value tempconv.Celsius, usage string) *tempconv.Celsius {
f := celsiusFlag{value}
flag.CommandLine.Var(&f, name, usage)
return &f.Celsius
}
var temp = tempconv.CelsiusFlag("temp", 20.0, "the temperature")
func main() {
flag.Parse()
fmt.Println(*temp)
}
相当于Set()方法用于从命令行中获取到-period
后面跟的值,然后进行处理;String()方法则是在需要输出的时候进行各式的转换。在这里的话是在fmt.Println(*temp)
调用String()的。
总而言之,如果说这个类想要调用flag.CommandLine.Var
这个方法修改参数的话,必须要实现flag.Value
这个接口才行。
具体流程:
- 先根据
flag.CommandLine.Var(&f, name, usage)
传入的name解析命令行(在flag.Parse()
的时候解析的) - 然后对这个
f
调用Set()
,传入的是跟在命令行name后的字符串。这样temp就相当于从命令行的标记中获取到传入的信息了。 - 调用
fmt.Println(*temp)
,这个时候用到了Strings()
。String方法格式化标记的值用在命令行帮助消息中。