golang 接口约定、类型、实现、flag.Value

接口

接口类型是对其它类型行为的抽象和概括。

接口约定

接口类型是一种抽象的类型,它不会暴露出内部值的结构和这个对象支持的基础操作。当你看到一个接口类型的值时,你不知道它是什么,但是你知道通过它的方法可以做什么。

比如2个相似的函数,实际上都是使用fmt.Fprintf这个函数封装的:
1. fmt.Printf:把结果打印到标准输出
2. fmt.Sprintf:把结果以字符串的形式返回

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类型,这是一个接口类型:

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

在上面的例子中:os.Stdout是一个*os.File类型,&buf是一个*bytes.Buffer类型。这2个类型都有实现一个Write函数。使用Fprintf的第一个参数接受任何类型,只要这些类型满足io.Write接口。

一个类型(*os.File)可以代替另一个满足相同接口的类型(*bytes.Buffer)使用,这种替换叫做可替换性(LSP里氏替换)。现在我们写一个新的类型并实现io.Write接口:

type ByteCounter int

func (c *ByteCounter) Write(p []byte) (int, error){
    *c = *c + ByteCounter(len(p))
    return len(p), nil
}

func main()  {
    var c ByteCounter
    // 这里相当于把hello world写入到了c指针里面
    fmt.Fprintf(&c, "hello world")
    fmt.Println(c)
}

接口类型

接口类型描述了一系列的方法,实现了这些方法的具体类型是这个接口类型的实例。Go语言中有单方法接口的命名的习惯。

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

type Closer interface {
    Close() error
}

与方法一样,也可以接口内嵌:

type ReadWriter interface {
    Reader
    Writer
}

实现接口

一个类型如果拥有一个接口需要的所有方法,那么这个类型就实现了这个接口。GO程序员经常把一个具体的类型描述成一个特定的接口类型,例如*bytes.Buffer是io.Write接口类型。

接口指定的规则非常简单,表达一个类型属于某个接口只要这个类型实现这个接口。

func main() {
    var w io.Writer
    w = os.Stdout
    w = new(bytes.Buffer)
}

上面都合法,因为os.Stdout和bytes.Buffer都实现了io.Writer接口;但是下面的会出错,这是因为bytes.Buffer并没有实现Closer接口;

var rwc io.ReadWriteCloser
    rwc = new(bytes.Buffer) //error:missing Close method
    fmt.Println(rwc)

更简单的描述:

w = rwc //成立,因为rwc实现了w接口
rwc = w //不成立,因为rwc需要实现r、w、c 3个接口

在方法中,我们知道某个具体类型T,它的一些方法的接受者可以是类型T本身,也可以是*T。这里需要注意的是假设某个接口方法是由*T指针实现的,并不能代表类型本身T也实现该接口方法:

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

var s IntSet
var _ fmt.Stringer = &s // OK
var _ fmt.Stringer = s  // compile error: IntSet lacks String method

上面的原因:由于只有 *IntSet 类型有String方法, 所以也只有 *IntSet 类型实现了fmt.Stringer的接口。

interface{}类型

interface{}被称为空接口类型,因为空接口类型对实现它的类型没有要求,所以我们可以将任意一个值赋给空接口类型。

var a interface{}

a = true
a = 12
a = "hello"

非空的接口类型比如io.Writer经常被指针类型实现,尤其当一个或多个接口方法像write一样隐式的给接收者带来变化的时候,一个结构体的指针是非常常见的承载方法的类型。

flag.Value接口

flag.Value可以帮助命令行标记定义新的符号。思考下面这个会休眠特定时间的程序:

var period = flag.Duration("period", 1*time.Second, "sp")

func main() {
    flag.Parse()
    fmt.Printf("sleep for %v", period)
    time.Sleep(*period)

}

默认情况下,休眠周期是一秒,但是可以通过 -period 这个命令行标记来控制。flag.Duration是实现time.Duration类型的标记变量。

$ go run t11.go -period 5s

自定义标记变量也很简单,只需要实现flag.Value接口的类型:

package flag

// Value is the interface to the value stored in a flag.
type Value interface {
    String() string
    Set(string) error
}
  1. String方法格式化标记命令行的帮助输出
  2. Set方法解析它的字符串参数并且更新标记变量的值

实现一个打印自己名字的类型:

type testFlag struct {
    Name string
}

func (f *testFlag) String() string {
    return fmt.Sprintf("%s", f.Name)
}

func (f *testFlag) Set(s string) error {
    f.Name = s
    return nil
}

func TestFlag(name string, value string, usage string) *testFlag {
    f := testFlag{value}
    flag.CommandLine.Var(&f, name, usage)
    return &f
}

func main() {
    var tf = TestFlag("name", "zx", "your name")
    flag.Parse()
    fmt.Println(tf.Name)
}

接口值

接口值由两个部分组成,一个具体的类型和那个类型的值。

下面4个语句中, 变量w得到了3个不同的值:

var w io.Writer
w = os.Stdout
w = new(bytes.Buffer)
w = nil
  1. 第一个语句定义了变量w,并被初始化,初始化零值为nil。
  2. 第二个语句将一个 *os.File 类型的值赋给变量w,这个赋值过程调用了一个具体类型到接口类型的隐式转换。通常在编译期我们不知道接口值的动态类型是什么,所以一个接口的调用必须动态分配。
  3. 第三个语句给接口值赋了一个*bytes.Buffer类型的值,现在动态类型是*bytes.Buffer并且动态值是一个指向新分配的缓冲区指针。
  4. 第四个语句把变量w恢复到和它之前定义时相同的状态

接口值可以使用==和!=来进行比较。两个接口值相等仅当它们都是nil值或者它们的动态类型相同并且动态值也根据这个动态类型的==操作相等。然而,如果两个接口值的动态类型相同,但是这个动态类型是不可比较的( 比如切片),将它们进行比较就会失败并且panic。

当我们处理错误或者调试的过程中,得知接口值的动态类型是非常有帮助的。所以我们使用fmt包的%T动作:

var w io.Writer
w = os.Stdout

fmt.Printf("%T", w)

猜你喜欢

转载自blog.csdn.net/andybegin/article/details/80942513