Go语言基础5 - 方法,接口,空白标识符,内嵌

概述

我们将用几节来学习Go语言基础,本文结构如下:

  1. 方法
    指针 vs. 值
  2.接口与其它类型
    接口
    类型转换
    接口转换与类型断言
    通用性
    接口和方法
  3.空白标识符
    多重赋值中的空白标识符
    未使用的导入和变量
    为副作用而导入
    接口检查
  4.内嵌

1.方法

指针 vs. 值

我们可以为任何已命名的类型(除了指针或接口)定义方法;
接收者可不必为结构体。

对于接收器,可以采用 指针或者指 ,通过下面的示例,我们先声明一个类型,再为它指定一个 值 类型的接收器。

type ByteSlice []byte

func (slice ByteSlice) Append(data []byte) []byte {
    // 主体省略
    return 切片返回值
}

注意上面的 方法 的返回值是个切片,它仍然需要返回更新后的切片。为了消除这种不便,我们可通过 将一个 指针 作为该方法的接收者, 示例:

func (p *ByteSlice) Append(data []byte) {
    slice := *p
    // 主体省略,但没有 return。
    *p = slice
}

这次的写法就不需要返回值了, 仿照 io.Writer 方法,我们继续改进它:

func (p *ByteSlice) Write(data []byte) (n int, err error) {
    slice := *p
    // 主体省略。
    *p = slice
    return len(data), nil
}

注意,这次它返回了两个值,一个表示长度,一个表示错误。
它满足了标准的 io.Writer 接口,这将非常实用。 例如,我们可以通过打印将内容写入。

var b ByteSlice
fmt.Fprintf(&b, "This hour has %d days\n", 7) // 注意这里传入地址

以指针或值为接收者的区别在于:值方法可通过指针和值调用, 而指针方法只能通过指针来调用。

指针方法可以修改接收者;通过值调用它们会导致方法接收到该值的副本, 因此任何修改都将被丢弃。不过有个方便的例外:若该值是可寻址的, 那么该语言就会自动插入取址操作符来对付一般的通过值调用的指针方法。在我们的例子中,变量 b 是可寻址的,因此我们只需通过 b.Write 来调用它的 Write 方法,编译器会将它重写为 (&b).Write。

2. 接口与其它类型

2.1 接口

接口为指定对象的行为提供了一种方法约定:

  • 如果某样东西可以完成这个, 那么它就可以用在这里。

比如:通过实现 String 方法,我们可以自定义打印函数。实现了 Write 方法的对象可被 Fprintf 使用来打印输出。

一种类型可以实现多个接口。

例如一个实现了 sort.Interface 接口的集合就可通过 sort 包中的例程进行排序。该接口包括 Len()、Less(i, j int) bool 以及 Swap(i, j int),还有个自定义的格式化字符串函数。

type Sequence []int

// Methods required by sort.Interface.
// sort.Interface 所需的方法。
func (s Sequence) Len() int {
    return len(s)
}
func (s Sequence) Less(i, j int) bool {
    return s[i] < s[j]
}
func (s Sequence) Swap(i, j int) {
    s[i], s[j] = s[j], s[i]
}

// Method for printing - sorts the elements before printing.
// 用于打印的方法 - 在打印前对元素进行排序。
func (s Sequence) String() string {
    sort.Sort(s)
    str := "["
    for i, elem := range s {
        if i > 0 {
            str += " "
        }
        str += fmt.Sprint(elem)
    }
    return str + "]"
}

2.2 类型转换

先看一个例子:

  func (s Sequence) String() string {
        sort.Sort(s)
        return fmt.Sprint([]int(s)) //注意这里
  }

注意上面的第三行,将 Sequence 转换为 []int 后,就能共享 []int 的 已实现的功能(被格式化输出)。

转换过程并不会创建新值,它只是值暂让现有的时看起来有个新类型而已。 (还有些合法转换则会创建新值,如从整数转换为浮点数等。)

2.3 接口转换与类型断言

类型选择

有时候要先判断后再安全转换,用到类型选择:它接受一个“接口“,在选择 (switch)中根据类型的不同来选择对应的情况(case), 然后再转换为该种类型,示例:

type Stringer interface {
    String() string
}

var value interface{} // 调用者提供的值。
switch str := value.(type) {
case string:
    return str
case Stringer:
    return str.String()
}

类型断言

有时候用到对单一类型判断,就用到类型断言。格式如下:

value.(typeName)

上面这句的 结果会返回:拥有静态类型 typeName 的新值。示例:

str := value.(string)

会将 value 转换成 string 后返回, str 是 字符串类型。

逗号, ok

如果不能转成字符串,这里就会错误崩溃。为避免这种情况, 需使用“逗号, ok”惯用方式,它能安全地判断该值是否为字符串:

str, ok := value.(string)
if ok {
    fmt.Printf("字符串值为 %q\n", str)
} else {
    fmt.Printf("该值非字符串\n")
}

若类型断言失败,str 将继续存在且为字符串类型,但它将拥有零值,即空字符串。

补充说明个的完整示例,像下面的 if-else 写法,它和上面说的”类型选择“”是一样:

if str, ok := value.(string); ok {
    return str
} else if str, ok := value.(Stringer); ok {
    return str.String()
}

2.4 通用性

若某种现有的类型仅实现了一个接口,且除此之外并无可导出的方法,则该类型本身就无需导出。 仅导出该接口能让我们更专注于其行为而非实现,其它属性不同的实现则能镜像该原始类型的行为。

这也能够避免为每个通用接口的实例重复编写文档。

2.5 接口和方法

[]

3. 空白标识符

空白标识符可被赋予或声明为任何类型的任何值,而其值会被无害地丢弃。

3.1 多重赋值中的空白标识符

如果某个函数返回多个值,我们使用多重赋值接收它,而其中某个变量不会被程序使用, 那么用空白标识符来代替该变量可避免创建无用的变量,并能清楚地表明该值将被丢弃。

if _, err := os.Stat(path); os.IsNotExist(err) {
    fmt.Printf("%s does not exist\n", path)
}

3.2 未使用的导入和变量

导入了某个包,或声明了某个变量而不使用它,就会产生编译错误。但实际编码过程中还会遇到 占位 的情况,先写一半代码通过编译再说。这时为了通过编译还得删除 占位 代码实在麻烦,这时,空白标识符有用了,示例:

package main

import (
    "fmt"
    "io"
    "log"
    "os"
)

func main() {
    fd, err := os.Open("test.go")
    if err != nil {
        log.Fatal(err)
    }
    // TODO: use fd.
}

上面的代码无法通过编译,我们使用空白标识符后,示例如下:

package main

import (
    "fmt"
    "io"
    "log"
    "os"
)

var _ = fmt.Printf // For debugging; delete when done. // 用于调试,结束时删除。
var _ io.Reader    // For debugging; delete when done. // 用于调试,结束时删除。

func main() {
    fd, err := os.Open("test.go")
    if err != nil {
        log.Fatal(err)
    }
    // TODO: use fd.
    _ = fd
}

注意,它使用了空白标识符来 调用了已导入包中的方法。使用空白标识符接收 未使用的变量 fd 来关闭未使用变量错误。

3.3 为副作用而导入

有时导入某个包只是为了其副作用, 而没有任何明确的使用。

只为了其副作用来哦导入该包, 只需将包重命名为空白标识符:

import _ "net/http/pprof"

上面的例子中,在 net/http/pprof 包的 init 函数中记录了HTTP处理程序的调试信息。它有个可导出的API, 但大部分客户端只需要该处理程序的记录和通过Web访问数据。

这种导入格式能明确表示该包是为其副作用而导入的,因为没有其它使用该包的可能: 在此文件中,它没有名字。(若它有名字而我们没有使用,编译器就会拒绝该程序。)

3.4 接口检查

若只需要判断某个类型是否是实现了某个接口,而不需要实际使用,可以使用空白标识符来忽略类型断言的值:

if _, ok := val.(json.Marshaler); ok {
    fmt.Printf("value %v of type %T implements json.Marshaler\n", val, val)
}

4. 内嵌

Go并不提供典型的,类型驱动的子类化概念,但通过 将类型 内嵌到结构体或接口中, 它就能“借鉴”部分实现。

4.1 接口内嵌

简单理解就是:将接口 嵌入到接口中。

接口内嵌非常简单,示例:

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

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

内嵌后:

// ReadWriter 接口结合了 Reader 和 Writer 接口。
type ReadWriter interface { // 这是个接口
    Reader  // 只有类型,没有变量
    Writer // 只有类型,没有变量
}

注意上面的写法,Reader 和 Writer 只有类型,没有变量

正如它看起来那样:ReadWriter 能够做任何 Reader 和 Writer 可以做到的事情,它是内嵌接口的联合体 (它们必须是不相交的方法集)。

只有接口能被嵌入到接口中。

4.2 结构体内嵌

4.2.1 结构体内嵌变量

我们先说 繁琐的实现如下:
内嵌的元素为指向结构体的指针,当然它们在使用前必须被初始化为指向有效结构体的指针。 ReadWriter 结构体可通过如下方式定义:

type ReadWriter struct { // 这里是结构体
    reader *Reader  // 注意这里有变量
    writer *Writer
}

但为了提升该字段的方法并满足 io 接口,我们同样需要提供转发的方法, 就像这样:

func (rw *ReadWriter) Read(p []byte) (n int, err error) {
    return rw.reader.Read(p)
}

如上,为了获得 子变量 的能力(功能),我们不得不在外部类写个同样的名字的方法作为转发到内部变量来调用。

然而,通过直接内嵌 “ 结构体类型 ”,我们就能避免如此繁琐,看下文。

4.2.2 结构体内嵌类型

// ReadWriter 存储了指向 Reader 和 Writer 的指针。
// 它实现了 io.ReadWriter。
type ReadWriter struct {   // 这里是结构体
    *Reader  // *bufio.Reader   注意这里是类型
    *Writer  // *bufio.Writer
}

在这里,内嵌类型的方法可以直接引用,这意味着 ReadWriter 不仅包括 Reader 和 Writer 的方法,它还同时满足下列三个接口: Reader、 Writer 以及 ReadWriter

  • 当内嵌一个类型时,该类型的方法会成为外部类型的方法,
  • 但当它们被调用时,该方法的接收者是内部类型,而非外部的。

在我们的例子中,当 ReadWriter 的 Read 方法被调用时,它与之前写的转发方法具有同样的效果;接收者是 ReadWriter 的 reader 字段,而非 ReadWriter 本身。

4.2.3 混合变量和类型的内嵌

这个例子展示了一个内嵌字段和一个常规的命名字段:

type Job struct {
    Command string
    *log.Logger
}

Job 类型现在有了 Log、Logf 和 *log.Logger 的其它方法。虽然可以为 Logger 提供一个字段名,但不必这么做。现在,一旦初始化后,我们就能调用 Job 的 Log 了:

job.Log("starting now...")

Logger 是 Job 结构体的常规字段, 因此我们可在 Job 的构造函数中,通过一般的方式来初始化它,就像这样:

func NewJob(command string, logger *log.Logger) *Job {
return &Job{command, logger}
}

或通过复合字面来构造:

job := &Job{command, log.New(os.Stderr, "Job: ", log.Ldate)}

若我们需要直接引用内嵌字段,可以忽略包限定名,直接将该字段的类型名作为字段名。
若我们需要访问 Job 类型的变量 job 的 *log.Logger, 可以直接写作 job.Logger。

func (job *Job) Logf(format string, args ...interface{}) {
    job.Logger.Logf("%q: %s", job.Command, fmt.Sprintf(format, args...))
}

4.2.4 命名冲突规则

内嵌类型会引入命名冲突的问题,但解决规则却很简单: 上层优先覆盖下层。

若相同的嵌套层级上出现同名冲突,通常会产生一个错误。比如当 Job 结构体中包含名为 Logger 的字段或方法,再将 log.Logger 内嵌到其中的话就会产生错误。然而,若重名永远不会在该类型定义之外的程序中使用,那就不会出错。 这种限定能够在外部嵌套类型发生修改时提供某种保护。
因此,就算添加的字段与另一个子类型中的字段相冲突,只要这两个相同的字段永远不会被使用就没问题。

END

发布了259 篇原创文章 · 获赞 7 · 访问量 9万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览