Golang中如何学习和使用Go的类型系统

Golang中如何学习和使用Go的类型系统 我编写了几个较小的Go应用程序,并学习了Go相关的书籍。但我仍然在Go的类型以及它们之间不同的转换路径上感到非常困惑。我该如何学习这些内容,并能够自信地编码,而不必复制/粘贴网上的代码?


这里有一个例子来说明我的困惑。在我的应用程序中,我使用了 NewDecoder(),它接受一个 io.Reader 参数。

我使用 http.Get() 发起了一个HTTP请求,根据我对文档的理解,它似乎返回一个 Response。该类型的 Body 字段返回一个 io.ReadCloser

因此,为了让两者都能工作,我需要从 io.ReadCloser 转换到 io.Reader。但是我在哪里能找到这个转换函数呢?

我在这里发现,从 io.ReadCloserio.Reader 不需要转换路径。但是,当我将 Response.Body 传递给 NewDecoder() 时,它报错“unexpected EOF”,所以我可能需要在它们之间使用一个能够正确处理EOF而不报错的读取器。🤔

早些时候,我让 NewDecoderos.Open() 一起工作,根据文档,它返回一个 File。但该文档页面没有定义结构体的字段,我在网上也找不到 File.Body 是什么类型。


编辑: 在这个具体案例中,我通过将一个 new(bytes.Buffer)ReadFrom() 函数放在HTTP请求和 NewDecoder() 之间,“修复”了这个问题。

这当然不是一个真正的解决方案,因为我只是通过反复试验、复制网上代码、花费大量时间(超过1小时)以及相当程度的挫败感才找到它的。

那么,我该如何学习Go的类型及其转换路径,从而获得更愉快、更高效的Go编程体验呢?


更多关于Golang中如何学习和使用Go的类型系统的实战教程也可以访问 https://www.itying.com/category-94-b0.html

8 回复

感谢您的回复和建议,Les。 🙂

更多关于Golang中如何学习和使用Go的类型系统的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


是的,这样会更符合惯例:将接口作为参数,返回具体类型。

为了快速掌握数据类型,制作一个速查表会很有帮助。在一个文件中记录下你遇到的每一个有趣/有用的结构体,并附上几句关于其功能的说明(例如一行代码示例)。

Juk:

你能推荐一些学习上述要点的资源吗?当然,反复试错也是可能的,但那会花费相当多的时间和带来挫败感。

在找到自己的方法之前,起步阶段是令人沮丧但必要的。尝试超越代码本身去思考,比如概念、编程结构等等。

资源方面,既然你在学习Go,可以从以下开始:

  1. Go标准库包Go编程语言文档 以及 https://gobyexample.com/
  2. 尝试解决简单问题来同时积累经验。例如,来自 https://projecteuler.net/ 的前10个问题。 2.1. 在解决问题时,每当你遇到难题,不要急于寻找答案,先尝试理解问题本身。如果你得到了答案,在“复制粘贴”之前,试着理解别人是如何解决这个问题的。有点像这些黑客拆解问题的方式:https://youtu.be/0uejy9aCNbI
  3. 书籍方面,有很多(GitHub - dariubs/GoBooks: Go语言书籍列表)。 4.1. 对我有用的一本(对于技术迁移来说非常高级但已过时)是:介绍 · 使用Golang构建Web应用 4.2. 编码中的常见实践:Effective Go - Go编程语言

再次感谢您详尽的回复。非常感谢你们两位花时间帮助一个素不相识的陌生人。🙂

skillian: 使用缓冲区有优点也有缺点。(…)

感谢您清晰的解释和示例。我能跟上您的解释,现在也意识到我之前在询问 Read() 函数冗余时犯了一个思维错误。

因为即使两种类型拥有相同签名的方法,Read() 的实际实现/行为当然是不同的。由于接口并不规定 Read() 内部应该做什么,而只是要求必须有一个具有特定签名的 Read() 方法,所以这两种方法仍然可以做略有不同的事情!

就像在您的例子中,BufferRead()ReaderRead() 在它们如何处理计算机内存以及何时可能出错方面是不同的。

再次感谢。🙂

skillian: Go 语言中的一个常见习惯是让函数接受接口作为参数并返回具体类型。这样,你的函数本质上就自我说明了它们期望参数拥有哪些方法。然后通过返回一个具体类型,你就可以完全访问该返回类型上所有可用的方法。

啊,我明白了。这很聪明,因为它也使函数在接受输入方面更加灵活,从而避免了代码重复的需要。

skillian: 虽然很小,但当你知道函数只会被调用特定类型的实例时,使用具体类型作为参数是合理的。

所以最好的方法是让函数接受类型输入。在我的应用程序中,例如有这样一个函数:

// getFileHash() opens a file and calculates that file's SHA256 hash
func getFileHash(path string) string { ... }

如果我这样做,会是更地道的 Go 用法吗:

// getFileHash() opens a file and calculates that file's SHA256 hash
func getFileHash(path Reader) string { ... }

(作为一个讨论示例,我无法使其正常工作,也找不到 string 具体类型是什么。)

hollowaykeanho: 资源方面,既然你在学习 Go,你可以从以下开始:

感谢这份列表!🙂

非常感谢这些积极、鼓舞人心且深入的回复!这真是太棒了。😊 我回来讨论这个话题时还有点担心,但即使是我父母对我也没这么好(开个玩笑,别担心)。

(我会就我有疑问的陈述进行回复,而不是把这个回复变成冗长的“感谢”回复。)

现在,要找到正确的“模具”和“积木”,你需要通过在使用前阅读包的文档来实现。根据包的不同,文档完全由包的维护者决定。

谢谢,这很有道理。需要多阅读并花时间理解。

简而言之,这是一门艺术:

  1. 分析、分解和理解问题
  2. 足智多谋,尤其是在搜索技巧方面。
  3. 推导出系统化、可重复的解决问题方法。

谢谢,我同意我在这方面还有很多要学的。你能推荐一些学习以上几点的资源吗?当然,试错也是可能的,但那会花费相当多的时间和带来挫败感。😊

接口类型是对具体类型必须存在以实现该接口的所需方法的描述。

谢谢。这对我来说很有道理,并且类似于我对C#接口的理解:“接口是实现某事的承诺/契约”。

也就是说,任何具有相同签名的Read方法(…)的具体类型,都实现了io.Reader

谢谢,所以如果我理解正确的话,类型的实现是隐式完成的?因为你说任何具有相同签名Read()方法的类型,都被认为是io.Reader,而无需明确声明?

一个接口类型(…)[是]仅仅是一个方法集合

这是一个非常棒的陈述,澄清了很多事情。

不需要转换,因为你正在从一个包含Read方法的接口类型转到另一个包含相同Read方法的接口类型。

非常感谢这个澄清。一直以来我都在寻找如何进行转换以及采取哪种转换路径的信息。我只需要寻找名称相似的方法?

在我的应用程序中,我通过给它一个来自new(bytes.buffer)的值来修复NewDecoder()。对于第一个参数是io.Reader接口,对于第二个参数是Buffer接口。现在我明白为什么它们是兼容的了!

根据文档,Buffer有这个方法是io.Reader所期望的。

func (b *Buffer) Read(p []byte) (n int, err error)

但我也可以使用strings包中的Reader.Read(),因为它有这个方

func (r *Reader) Read(b []byte) (n int, err error)

如果我的理解是正确的,那么我选择哪个选项有关系吗?一个比另一个更好吗?或者这种冗余仅仅是为了方便而存在?

编译器知道ioutil.ReadAll需要一个实现了io.Reader方法集的值,这与myReader的方法集相同,所以它就能工作。

所以类型并不重要,因为ioutil.ReadAll()只是想要一个具有特定签名的Read()方法就满足了?你可以创建任何类型,只要Read()存在,一切都没问题。

这意味着我寻找类型转换实际上有点傻,因为Go并不关心这个。这简化了,但:Go只是想将方法匹配在一起。

Juk:

那么我该如何学习Go的类型和转换路径,

你不可能记住世界上所有的Go类型😂。为了基本理解,目前你只需要记住“类型必须匹配类型”,就像方形积木只能放入方形模具,而不是圆形或三角形。

这背后的原因是对计算内存的防御性控制,这本身就可以成为计算机科学的一个独立主题。

诀窍在于,如何在需要时找到正确的“模具”和“积木”。

Juk:

我如何学习这些知识并自信地编码,而不必复制/粘贴网上的代码?

现在,要找到正确的“模具”和“积木”,你需要在使用前阅读包的文档。根据包的不同,文档完全由包的维护者决定。以下是一些例子:

  1. 标准包的文档在这里:Standard library - Go Packages
  2. 社区贡献的包将遵循维护者的文档。例如: GitHub - goreleaser/nfpm: nFPM is Not FPM - a simple deb, rpm, apk and arch linux packager written in Gohttps://nfpm.goreleaser.com/
  3. 如果其他方法都失败了,而源代码可供阅读(例如开源),你最后的手段就是阅读代码。

首先去哪里找?提高你的Google / DuckDuckGo搜索技巧。

在论坛提问(取决于哪个论坛)有时会招致非常刻薄和令人沮丧的回复,以至于你可能会质疑我们的社会怎么了。

Juk:

所以为了让两者都能工作,我需要从 io.ReadCloserio.Reader。但是我在哪里能找到那个函数?

假设你对io包的ReadCloser(...)Reader(...)函数一无所知,首先要做的是通过搜索找到io包(io package - io - Go Packages)。你会发现它们都被称为interface类型,这指引你去理解Go中interface是如何工作的。因此,继续去寻找必要的文档并通过理解来消化它们。

Juk:

这样我就能拥有更愉快、更高效的Go体验了吗?

你问的是一个基于经验的问题。因此,没有“一种”解决方案能解决所有问题。我能告诉你的是,你还没有找到学习编程范式的方法。要有耐心,探索你自己的道路。

就我而言,我更喜欢阅读文本、代码和研究论文(主要是因为我能够快速阅读以及过去的经验),同时进行动手实验。因此,尝试 https://gobyexample.com/ 并通读 Documentation - The Go Programming Language 中所有枯燥的规范对我来说非常有效。

有些人受不了这样,他们更喜欢通过阅读指导书籍、视频教程或参加训练营来获得指导。有些人是天生的天才,无需先参考任何东西就能吸收一切(到目前为止,我一生中只实际遇到过2个这样的人)。

Juk:

这当然不是一个真正的解决方案,因为我只是通过反复试验、复制网上代码、大量时间(1小时以上)和相当多的挫折才找到的。

因为编程从一开始就不是为了“复制和粘贴”。

简而言之,这是一门艺术:

  1. 分析、分解和理解问题
  2. 足智多谋,尤其是在搜索技巧方面。
  3. 推导出系统化、可重复的解决问题的方法。

要精通一门艺术需要时间,所以不要放弃。继续你的探索,需要时休息一下🙃。

我将对Holloway的回答进行补充,提供一些希望有用的信息:

Go语言中的类型有两种“分类”*:具体类型和接口类型。

具体类型包括:

  • 所有Go的内置类型,如boolintstring等。
  • 所有的struct
  • 所有的数组
  • 所有的指针
  • 所有的切片
  • 所有的映射
  • 所有的函数

也就是说,具体类型就是一切。Go中所有有类型的东西都属于具体类型。如果你想从一种具体类型转换到另一种,必须使用类型转换。

接口类型是对具体类型必须存在的方法集合的描述,以满足接口的实现。例如:

io.Reader是一个接口类型,因此只包含一个方法集合:

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

也就是说,任何拥有相同签名(一个[]byte切片参数并返回(int, error))的Read方法的具体类型,都实现了io.Readerio.ReadCloser也是一个接口类型:

type ReadCloser interface {
        Read(b []byte) (n int, err error)
        Close() error
}

因为io.ReadCloser接口定义了与io.Reader相同的Read方法,所以如果你的类型当前实现了io.Reader,并且你添加了一个返回errorClose方法,那么你的类型也就实现了io.ReadCloser

再进一步整合一下:

http.Response类型的Body字段是io.ReadCloser类型。我们现在知道io.ReadCloser只是一个方法集合。你实际上永远不会有一个类型为io.ReadCloser,这个必须有一个真实的、具体的类型,并且该类型必须拥有ReadClose方法,就像io.ReadCloser接口所要求的那样。

如果你有一个值(属于某种具体类型,我们实际上并不关心是哪种)实现了io.ReadCloser,并且你想将它用作io.Reader,那么不需要进行转换,因为你正在从一个包含Read方法的接口类型转换到另一个包含相同Read方法的接口类型。

考虑以下示例:

package main

import (
	"fmt"
	"io/ioutil"
)

type myType struct{}

func (myType) Read(a []byte) (b int, err error) {
	return 0, fmt.Errorf("TODO!")
}

type myReader interface {
	Read(b []byte) (n int, err error)
}

func main() {
	var x myReader = myType{}
	bs, err := ioutil.ReadAll(x)
	fmt.Println("bs:", bs, "err:", err)
}

我在这里无缘无故地创建了自己的myReader类型,只是为了演示接口类型只定义方法集合,并且如果编译器在编译时知道我的x变量是某种实现了myReader方法集合的类型,那么编译器就知道ioutil.ReadAll需要一个实现了io.Reader方法集合的值,而这个集合与myReader相同,所以它就能正常工作。

一个*os.File没有Body字段,因为*os.File本身已经拥有ReadClose方法。


  • “分类”这个词是我在这里选择的,并不是这种划分的官方术语,所以我怀疑你在其他地方不会看到它被这样称呼。

在Go中理解类型系统的关键在于接口的隐式实现和类型嵌入机制。你的具体问题实际上展示了Go类型系统的一个优雅特性。

关于io.ReadCloserio.Reader的"转换":

package main

import (
    "encoding/json"
    "fmt"
    "io"
    "net/http"
)

func main() {
    resp, err := http.Get("https://api.example.com/data")
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close() // 重要:记得关闭
    
    // 这里不需要任何转换 - io.ReadCloser 就是 io.Reader
    decoder := json.NewDecoder(resp.Body)
    
    var data map[string]interface{}
    if err := decoder.Decode(&data); err != nil {
        fmt.Println("解码错误:", err)
        return
    }
    
    fmt.Println(data)
}

io.ReadCloser的定义:

type ReadCloser interface {
    Reader
    Closer
}

这意味着任何实现了Read()Close()方法的类型都自动满足io.ReadCloser接口,同时也满足io.Reader接口。这就是Go接口的隐式实现。

关于os.File的例子:

package main

import (
    "encoding/json"
    "fmt"
    "os"
)

func main() {
    file, err := os.Open("data.json")
    if err != nil {
        panic(err)
    }
    defer file.Close()
    
    // *os.File 实现了 io.Reader 接口
    decoder := json.NewDecoder(file)
    
    var data map[string]interface{}
    if err := decoder.Decode(&data); err != nil {
        fmt.Println("解码错误:", err)
        return
    }
    
    fmt.Println(data)
}

理解Go类型转换的几个关键点:

  1. 接口断言
var r io.Reader = resp.Body
if rc, ok := r.(io.ReadCloser); ok {
    // rc 是 io.ReadCloser 类型
    rc.Close()
}
  1. 类型转换(仅适用于兼容类型):
var i int32 = 100
var j int64 = int64(i) // 显式类型转换
  1. 理解标准库中的接口层次
// io包中的接口关系
type Reader interface {
    Read(p []byte) (n int, err error)
}

type Closer interface {
    Close() error
}

type ReadCloser interface {
    Reader
    Closer
}

你遇到的"unexpected EOF"错误通常是因为HTTP响应体为空或读取时出现问题,而不是类型转换问题。正确的错误处理应该是:

resp, err := http.Get("https://api.example.com/data")
if err != nil {
    return err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
    return fmt.Errorf("HTTP错误: %s", resp.Status)
}

decoder := json.NewDecoder(resp.Body)
// ... 解码逻辑

学习Go类型系统的最佳方式是:

  • 阅读标准库源码,特别是ionet/http等常用包
  • 使用go doc命令查看类型定义:go doc io.ReadCloser
  • 理解接口的隐式实现机制
  • 实践编写自定义接口和实现

你的困惑是正常的,Go的类型系统虽然简洁但需要时间适应。通过直接阅读标准库文档和源码,你会逐渐建立对类型关系的直觉理解。

回到顶部