Golang中如何学习和使用Go的类型系统
Golang中如何学习和使用Go的类型系统 我编写了几个较小的Go应用程序,并学习了Go相关的书籍。但我仍然在Go的类型以及它们之间不同的转换路径上感到非常困惑。我该如何学习这些内容,并能够自信地编码,而不必复制/粘贴网上的代码?
这里有一个例子来说明我的困惑。在我的应用程序中,我使用了 NewDecoder(),它接受一个 io.Reader 参数。
我使用 http.Get() 发起了一个HTTP请求,根据我对文档的理解,它似乎返回一个 Response。该类型的 Body 字段返回一个 io.ReadCloser。
因此,为了让两者都能工作,我需要从 io.ReadCloser 转换到 io.Reader。但是我在哪里能找到这个转换函数呢?
我在这里发现,从 io.ReadCloser 到 io.Reader 不需要转换路径。但是,当我将 Response.Body 传递给 NewDecoder() 时,它报错“unexpected EOF”,所以我可能需要在它们之间使用一个能够正确处理EOF而不报错的读取器。🤔
早些时候,我让 NewDecoder 与 os.Open() 一起工作,根据文档,它返回一个 File。但该文档页面没有定义结构体的字段,我在网上也找不到 File.Body 是什么类型。
编辑: 在这个具体案例中,我通过将一个 new(bytes.Buffer) 和 ReadFrom() 函数放在HTTP请求和 NewDecoder() 之间,“修复”了这个问题。
这当然不是一个真正的解决方案,因为我只是通过反复试验、复制网上代码、花费大量时间(超过1小时)以及相当程度的挫败感才找到它的。
那么,我该如何学习Go的类型及其转换路径,从而获得更愉快、更高效的Go编程体验呢?
更多关于Golang中如何学习和使用Go的类型系统的实战教程也可以访问 https://www.itying.com/category-94-b0.html
是的,这样会更符合惯例:将接口作为参数,返回具体类型。
为了快速掌握数据类型,制作一个速查表会很有帮助。在一个文件中记录下你遇到的每一个有趣/有用的结构体,并附上几句关于其功能的说明(例如一行代码示例)。
Juk:
你能推荐一些学习上述要点的资源吗?当然,反复试错也是可能的,但那会花费相当多的时间和带来挫败感。
在找到自己的方法之前,起步阶段是令人沮丧但必要的。尝试超越代码本身去思考,比如概念、编程结构等等。
资源方面,既然你在学习Go,可以从以下开始:
- Go标准库包、Go编程语言文档 以及 https://gobyexample.com/
- 尝试解决简单问题来同时积累经验。例如,来自 https://projecteuler.net/ 的前10个问题。 2.1. 在解决问题时,每当你遇到难题,不要急于寻找答案,先尝试理解问题本身。如果你得到了答案,在“复制粘贴”之前,试着理解别人是如何解决这个问题的。有点像这些黑客拆解问题的方式:https://youtu.be/0uejy9aCNbI
- 书籍方面,有很多(GitHub - dariubs/GoBooks: Go语言书籍列表)。 4.1. 对我有用的一本(对于技术迁移来说非常高级但已过时)是:介绍 · 使用Golang构建Web应用 4.2. 编码中的常见实践:Effective Go - Go编程语言
再次感谢您详尽的回复。非常感谢你们两位花时间帮助一个素不相识的陌生人。🙂
skillian: 使用缓冲区有优点也有缺点。(…)
感谢您清晰的解释和示例。我能跟上您的解释,现在也意识到我之前在询问 Read() 函数冗余时犯了一个思维错误。
因为即使两种类型拥有相同签名的方法,Read() 的实际实现/行为当然是不同的。由于接口并不规定 Read() 内部应该做什么,而只是要求必须有一个具有特定签名的 Read() 方法,所以这两种方法仍然可以做略有不同的事情!
就像在您的例子中,Buffer 的 Read() 与 Reader 的 Read() 在它们如何处理计算机内存以及何时可能出错方面是不同的。
再次感谢。🙂
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,你可以从以下开始:
感谢这份列表!🙂
非常感谢这些积极、鼓舞人心且深入的回复!这真是太棒了。😊 我回来讨论这个话题时还有点担心,但即使是我父母对我也没这么好(开个玩笑,别担心)。
(我会就我有疑问的陈述进行回复,而不是把这个回复变成冗长的“感谢”回复。)
现在,要找到正确的“模具”和“积木”,你需要通过在使用前阅读包的文档来实现。根据包的不同,文档完全由包的维护者决定。
谢谢,这很有道理。需要多阅读并花时间理解。
简而言之,这是一门艺术:
- 分析、分解和理解问题
- 足智多谋,尤其是在搜索技巧方面。
- 推导出系统化、可重复的解决问题方法。
谢谢,我同意我在这方面还有很多要学的。你能推荐一些学习以上几点的资源吗?当然,试错也是可能的,但那会花费相当多的时间和带来挫败感。😊
接口类型是对具体类型必须存在以实现该接口的所需方法的描述。
谢谢。这对我来说很有道理,并且类似于我对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只是想将方法匹配在一起。
我将对Holloway的回答进行补充,提供一些希望有用的信息:
Go语言中的类型有两种“分类”*:具体类型和接口类型。
具体类型包括:
- 所有Go的内置类型,如
bool、int、string等。 - 所有的
struct - 所有的数组
- 所有的指针
- 所有的切片
- 所有的映射
- 所有的函数
也就是说,具体类型就是一切。Go中所有有类型的东西都属于具体类型。如果你想从一种具体类型转换到另一种,必须使用类型转换。
接口类型是对具体类型必须存在的方法集合的描述,以满足接口的实现。例如:
io.Reader是一个接口类型,因此只包含一个方法集合:
type Reader interface {
Read(b []byte) (n int, err error)
}
也就是说,任何拥有相同签名(一个[]byte切片参数并返回(int, error))的Read方法的具体类型,都实现了io.Reader。io.ReadCloser也是一个接口类型:
type ReadCloser interface {
Read(b []byte) (n int, err error)
Close() error
}
因为io.ReadCloser接口定义了与io.Reader相同的Read方法,所以如果你的类型当前实现了io.Reader,并且你添加了一个返回error的Close方法,那么你的类型也就实现了io.ReadCloser。
再进一步整合一下:
http.Response类型的Body字段是io.ReadCloser类型。我们现在知道io.ReadCloser只是一个方法集合。你实际上永远不会有一个类型为io.ReadCloser的值,这个值必须有一个真实的、具体的类型,并且该类型必须拥有Read和Close方法,就像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本身已经拥有Read和Close方法。
- “分类”这个词是我在这里选择的,并不是这种划分的官方术语,所以我怀疑你在其他地方不会看到它被这样称呼。
在Go中理解类型系统的关键在于接口的隐式实现和类型嵌入机制。你的具体问题实际上展示了Go类型系统的一个优雅特性。
关于io.ReadCloser到io.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类型转换的几个关键点:
- 接口断言:
var r io.Reader = resp.Body
if rc, ok := r.(io.ReadCloser); ok {
// rc 是 io.ReadCloser 类型
rc.Close()
}
- 类型转换(仅适用于兼容类型):
var i int32 = 100
var j int64 = int64(i) // 显式类型转换
- 理解标准库中的接口层次:
// 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类型系统的最佳方式是:
- 阅读标准库源码,特别是
io、net/http等常用包 - 使用
go doc命令查看类型定义:go doc io.ReadCloser - 理解接口的隐式实现机制
- 实践编写自定义接口和实现
你的困惑是正常的,Go的类型系统虽然简洁但需要时间适应。通过直接阅读标准库文档和源码,你会逐渐建立对类型关系的直觉理解。


