快速结合io.reader和io.closer的方法
#go #todayilearned #code

Golang的标准库中有许多有趣的工具包裹koude0实例,例如koude1koude2。但是,当包装koude3实例时,Close方法被隐藏了。

这是一个快速代码段,可以通过Inline struct组合包裹的koude0和原始的koude6来重建koude6接口。

代码

var rc io.ReadCloser = struct {
    io.Reader
    io.Closer
}{
    Reader: r,
    Closer: c,
}

这是什么?

Golang中的koude0接口在流数据时是非常强大的抽象。它可用于读取文件,http响应,甚至是使用通用代码的原始字节数组:

func sumAllBytes(r io.Reader) (uint64, error) {
    var buff [512]byte
    var sum uint64

    for {
        n, err := r.Read(buff[:])
        for i := 0; i < n; i++ {
            sum += uint64(buff[i])
        }

        if errors.Is(err, io.EOF) {
            return sum, nil
        }
        if err != nil {
            return sum, err
        }
    }
}

该方法可用于处理文件,http响应,甚至内存缓冲区:

func main() {

    // Process a buffer
    buff := bytes.NewReader([]byte{0x01, 0x02, 0x03, 0x04})

    sum, err := sumAllBytes(buff)
    if err != nil {
        log.Fatal(err)
    }
    log.Println("Sum from buffer:", sum)

    // Process a http response

    resp, err := http.Get("https://www.google.com")
    if err != nil {
        log.Fatal(err)
    }
    defer resp.Body.Close()

    sum, err = sumAllBytes(resp.Body)
    if err != nil {
        log.Fatal(err)
    }
    log.Println("Sum from http:", sum)

    // Process a file

    fl, err := os.Open("/some/file/path")
    if err != nil {
        log.Fatal(err)
    }
    defer fl.Close()

    sum, err = sumAllBytes(fl)
    if err != nil {
        log.Fatal(err)
    }
    log.Println("Sum from file:", sum)
}

打扫干净

在许多情况下(例如上面的HTTP和文件实例),必须明确关闭读者以避免资源泄漏。因此,许多资源都使用koude3接口,并且可以通过defer rc.Close()语句轻松地清理。但是在某些情况下,关闭方法在同一函数上未调用,而是在呼叫者站点的某个地方调用。在该构造中,呼叫者负责清理:

func getDataStream(name string) (io.ReadCloser, error) {
    switch name {
    case "file":
        return os.Open("/some/file")
    case "http":
        resp, err := http.Get("https://www.google.com/")
        if err != nil {
            return nil, err
        }
        return resp.Body, nil
    case "buffer":
        return io.NopCloser(
            bytes.NewReader([]byte{0x01, 0x02, 0x03, 0x04}),
        ), nil
    default:
        return nil, errors.New("Invalid data stream name")
    }
}

func printSum(streamName string) {
    stream, err := getDataStream(streamName)
    if err != nil {
        log.Fatal(err)
    }
    defer stream.Close()

    sum, err := sumAllBytes(stream)
    if err != nil {
        log.Fatal(err)
    }

    log.Printf("Sum of bytes in stream '%s' is '%d\n", streamName, sum)
}

到目前为止一切都很好,没什么可担心的。但是,让我们通过一些流包装扩展此示例:

func getDataStream(name string) (io.ReadCloser, error) {
    switch name {
    case name == "file":
        return os.Open("/some/file")
    case name == "http":
        resp, err := http.Get("https://www.google.com/")
        if err != nil {
            return nil, err
        }
        return resp.Body, nil
    case name == "buffer":
        return io.NopCloser(
            bytes.NewReader([]byte{0x01, 0x02, 0x03, 0x04}),
        ), nil

    // v---  Create a truncated stream by applying limit over the base one ---v
    case strings.HasPrefix(name, "limit:"):
        r, err := getDataStream(name[6:])
        if err != nil {
            return nil, err
        }
        return io.LimitReader(r, 3)

    default:
        return nil, errors.New("Invalid data stream name")
    }
}

不幸的是,此代码没有编译,最终出现此错误:

cannot use io.LimitReader(r, 100) (value of type io.Reader) as type io.ReadCloser in return statement:
    io.Reader does not implement io.ReadCloser (missing Close method)

koude1koude3包裹起来确实隐藏了原始实例的koude6功能。事实证明,Golang Standard Lib有很多地方发生这种包装。

内联结构到营救

有一个简单的技巧可以将Close方法从原始读取器带回包装的方法:

func getDataStream(name string) (io.ReadCloser, error) {
    switch name {
    case name == "file":
        return os.Open("/some/file")
    case name == "http":
        resp, err := http.Get("https://www.google.com/")
        if err != nil {
            return nil, err
        }
        return resp.Body, nil
    case name == "buffer":
        return io.NopCloser(
            bytes.NewReader([]byte{0x01, 0x02, 0x03, 0x04}),
        ), nil

    // v---  Create a truncated stream by applying limit over the base one ---v
    case strings.HasPrefix(name, "limit:"):
        r, err := getDataStream(name[6:])
        if err != nil {
            return nil, err
        }

        limitReader := io.LimitReader(r, 3)

        return struct {
            io.Reader
            io.Closer
        }{
            Reader: limitReader, // Read method will come from the wrapped reader
            Closer: r,           // Close method will come from the original reader
        }, nil

    default:
        return nil, errors.New("Invalid data stream name")
    }
}

它是如何工作的?

inline struct包含两个embedded fields,一个用于读者,另一个用于近距离。

由于这些字段是匿名的,因此struct本身 从这些字段中方法,就好像在结构上声明了这些字段一样。通过这样做,每当编译器试图将结构施放到某个接口时,它都可以proplation 这些方法满足接口的要求。

在上面的代码中,我们返回一个需要ReadClose方法的koude3接口的实例 - 分别是从嵌入式字段借用的。

有趣的是,如果我们将整个koude3用作第二个嵌入式字段,而不是koude0,则编译器(在书面上为1.19)会引起由于晋升的现场成员之间的歧义引起的错误(Read方法没有促进歧义):

cannot use struct{io.Reader; io.ReadCloser}{…} (value of type struct{io.Reader; io.ReadCloser}) as type io.ReadCloser in return statement:
    struct{io.Reader; io.ReadCloser} does not implement io.ReadCloser (missing Read method)