本文永久链接 – https://tonybai.com/go-advanced-course-faq

《TonyBai · Go 语言进阶课》专栏于2025年5月12日正式上线了!和《Go语言第一课》专栏一样,我也在这里建立一个页面,用于汇总读者的常见的精彩提问以及我的回答,作为我和专栏学习者基于专栏的二次创作,供广大的专栏学习者阅读参考。

本页面内容将持续更新!请关注本FAQ永久链接 – https://tonybai.com/go-advanced-course-faq。

一. 本人相关

  • 新的进阶课程是AI朗读?不是您的声音了吗?

2022年ChatGPT大模型应用上线以来,文字转音频日益成熟,如今极客时间专栏已经全面采用AI机器人朗读模式,我的专栏并非个例。

二. Go进阶专栏

  • 为什么要出这个Go进阶专栏?

为此,我特意写了一篇简短的文章,叙述了这门Go专栏诞生幕后的那些事,感兴趣的朋友可以去看看。

  • 专栏的更新节奏

根据极客时间要求,专栏依然是每周更新三篇!

  • 是否有针对该专栏的交流群

目前暂没有。作者精力有限,能力有限,不适合维护这样的一个群,希望大家体谅。欢迎大家在专栏积极留言,我会认真解答大家问题的。

  • 专栏讲解使用的是Go最新稳定版本吗?

专栏的内容使用的是Go 1.24版本,这是Go团队与2025年2月份发布的最新Go稳定版。

  • 专栏课程相关源码从哪里可以下载到?

下面为专栏源码专用仓库地址:

github仓库 – https://github.com/bigwhite/publication/tree/master/column/timegeek/go-advanced-course

  • 我以前买了Go语言第一课,这个新进阶课程与第一课重复的内容多么?

《Go语言第一课》和《Go语言进阶课》是为处于不同学习阶段的 Gopher 设计的,目标也截然不同。

《第一课》 更侧重于帮助初学者或有其他语言背景的同学快速入门 Go 语言,掌握其核心语法、常用库和基本并发编程,目标是让你能“写出能跑的 Go 程序”。

而《进阶课》 则是面向已经具备 Go 基础(比如完成了《第一课》或有同等水平)的同学,目标是帮助大家从“熟练”到“精通”。它会深入探讨《第一课》中可能仅点到为止或未曾涉及的底层原理、设计哲学、工程实践、性能调优、复杂项目的设计与架构等。

所以,即使在目录中看到一些相似的关键词(比如“切片”、“Map”、“并发”),《进阶课》也会从更深、更广、更偏实践和设计的角度去剖析,解决的是你在实际工作中遇到的更复杂的问题和瓶颈。可以说,两者的内容重复性非常低,它们是承上启下的关系,而非简单的重复。

  • 我买了《Go精进之路》1、2册,还需要买这个进阶课程吗

《Go语言精进之路》书籍和这门《TonyBai · Go 语言进阶课》虽然都旨在帮助大家深入 Go,但侧重点和内容有显著不同:

  1. 语法强化方面:两者确实会有一些基础概念的交集。但《精进之路》书籍更侧重于对 Go 语法特性本身的深度剖析和原理阐述。而《进阶课》虽然也夯实语法,但更偏重于这些语法特性在进阶场景下的应用、认知瓶颈的突破以及底层逻辑的实践性理解,视角和深度有所不同。
  2. 设计与工程实践方面:这部分是《进阶课》的核心增量和全新内容。课程中的“设计先行”和“工程实践”两大模块,涵盖了从项目布局、包设计、并发设计、接口设计,到应用骨架、可观测性、性能调优、云原生部署、AI 集成等几乎在《精进之路》中未曾系统展开的实战内容。这块是课程独特价值的关键所在。
  3. 时效性:这是线上课程的一大优势。《进阶课》的内容已经同步到了最新的 Go 版本1.24,确保了知识和实践的前沿性。

简单来说,如果您在阅读《精进之路》后,希望系统提升软件设计能力、掌握生产级服务的工程化方法,并了解 Go 的最新实践,那么《进阶课》将为您带来全新的、极具价值的内容。两者是很好的互补。

  • 老师,你有计划将这进阶课的内容出书吗?

关于出书计划,目前我的主要精力还是放在确保专栏内容的质量和与大家的线上互动上。《Go语言进阶课》专栏刚刚在极客时间上架,我希望能先通过专栏的形式,与大家充分交流,收集反馈,持续打磨内容,让它能最大限度地帮助到正在进阶路上的 Gopher 们。所以,短期内暂时还没有将《进阶课》内容整理出书的计划。

不过,极客时间平台一直以来都非常支持讲师 IP 的价值最大化,对于作者将优质专栏内容出版成书也持非常开放和鼓励的态度。未来,如果时机成熟,并且《进阶课》的内容经过了充分的沉淀和迭代,我一定会认真考虑将其出版成书,以满足不同读者的学习需求。

三. 专栏内容答疑

  • 方法值和方法表达式在什么样的开发场景中使用?感觉很少使用,或许是不知道该什么使用?

这个问题问得特别好,确实,方法值和方法表达式不像 for 循环或 if 语句那样天天用。但它们在特定的场景下能让代码变得简洁和优雅一些。

方法值将一个方法绑定到了一个具体实例上,形成一个函数。就像创建了一个快捷方式,点一下就直接对那个特定对象执行操作。其核心在于“将一个特定实例的方法当作一个值来传递”。

一个常见的场景是用作回调函数或事件处理器。我们常用的net/http就是一个很好的例子。

type APIServer struct {
    addr string
    // ... 其他依赖,如数据库连接
}

// handleRoot是APIServer的一个方法,它可以访问APIServer的内部状态
func (s *APIServer) handleRoot(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Welcome to the server at %s", s.addr)
}

func main() {
    server := &APIServer{addr: ":8080"}

    // 这里,server.handleRoot就是一个方法值!
    // 它是一个函数,已经绑定了server这个实例。
    // 当请求来了,http库直接调用这个函数即可。
    http.HandleFunc("/", server.handleRoot)

    http.ListenAndServe(server.addr, nil)
}

另外一个大家可能没有意思到的典型场景,那就是在 Goroutine 中执行特定实例的任务。

type Worker struct {
    id   int
    task string
}

func (w *Worker) Process() {
    fmt.Printf("Worker %d is processing task: %s\n", w.id, w.task)
    // ... 模拟长时间工作
    time.Sleep(2 * time.Second)
    fmt.Printf("Worker %d finished.\n", w.id)
}

func main() {
    w1 := &Worker{id: 1, task: "data-crunching"}
    w2 := &Worker{id: 2, task: "image-resizing"}

    // go w1.Process 就是在使用方法值
    // 启动一个goroutine,专门执行w1的Process方法
    go w1.Process()
    go w2.Process()

    time.Sleep(3 * time.Second)
}

方法表达式的核心在于“将方法本身作为一种算法策略来使用”,通常用在需要对一组同类型对象进行统一操作的场景。

典型的使用场景,包括用于高阶函数。 比如:sort.Slice 或自定义的集合操作

type User struct {
    Name string
    Age  int
}

// IsOlderThan 是一个方法
func (u User) IsOlderThan(other User) bool {
    return u.Age > other.Age
}

// FindFirst(users, predicate) 这样的函数
// 但最经典的还是用在排序的场景
func main() {
    users := []User{{"Alice", 30}, {"Bob", 25}, {"Charlie", 35}}

    // 我们想按年龄降序排序
    sort.Slice(users, func(i, j int) bool {
        // 在这个闭包内部,我们可以使用方法值
        //return users[i].IsOlderThan(users[j])

        // 或者,我们可以用方法表达式来实现
        return User.IsOlderThan(users[i], users[j])
    })
    fmt.Println(users)
}

总的来说,当你需要把某个特定对象的某个方法传来传去时,用方法值。当你需要把一类对象的某个通用方法当作一个算法或策略来使用时,用方法
表达式。

  • 究竟该不该一直在消费者包中定义接口呢?

软件设计中一个永恒的主题:原则是指导,实践需权衡。

你问(第20讲):“究竟该不该一直在消费者包中定义接口呢?”

答案是:不一定。这取决于场景和你的设计目标。

“接口定义在消费者”为什么好?

  • 最大化解耦: 消费者(比如 handler)只定义它自己需要的最小接口,它不关心谁来实现,也不需要 import 任何具体的实现包。
  • 易于测试: 消费者只依赖自己包内的接口,写单元测试时,mock实现变得极其简单。
  • 强制我们从使用者的角度思考需要什么能力,而不是从实现者的角度思考能提供什么能力。

为什么第20讲的实战项目中没有完全遵守?更具体说:为什么 storage 和 idgen 的接口没有定义在它们的消费者(比如 shortener/service.go)里,而是定义在了 storage/interface.go 和 idgen/interface.go?

这背后有两个非常务实和重要的考量:

考量一:将包视为一个“功能模块”或“子系统”

在我们的项目中,storage 不仅仅是一个包,它代表了一个完整的“存储”子系统。这个子系统对外提供了一套标准的“契约”(Contract),而 storage/interface.go 里的 Repository 接口,就是这个存储子系统的公开API。 同样,idgen 是“ID生成”子系统,idgen.Generator 接口就是它的公开API。

任何想了解“存储”能力的人,只需查看 internal/storage 目录,特别是 interface.go,就能立刻明白这个模块能做什么。这极大地提升了代码的可读性和可维护性。

考量二:应对“多消费者”场景

一个接口往往有多个消费者。当有多个消费者时,这个接口应该定义在哪呢?定义在A包中,那么同样使用该接口的B包就要import A,造成不必要,也是不合理的耦合。

解决方案就是将这个被多个消费者共享的接口,提升到一个公共的、稳定的地方。最自然的地方,就是它所描述的那个功能模块的根目录下,即 storage/interface.go。

  • 针对“测试进阶:组织、覆盖、Mock与Fuzzing的最佳实践”这节课,课中的一切推荐的策略和规范,只需要引入一个ginkgo 框架,这样可能会更适
    合一个组织来规范测试代码?

这个问题提得非常好,直接点到了Go测试的一个核心选择:用原生库还是上框架?

你说的没错,ginkgo 这样的BDD框架确实能很强地规范团队的测试风格,它的结构化和功能非常清晰。

但我们这门课的核心目标,更像是 “授人以渔”。

ginkgo 就像一条已经烹饪好的“鱼”,它好吃、方便,直接解决了“怎么规范组织和编写测试”的问题。

而我们的专栏里讲解的测试组织的规范和策略等这些,则是“渔”——是钓鱼的方法和工具。

目的是让大家:

打好基本功: 掌握了这些基础,你才能真正理解 ginkgo 这类框架在背后为你做了什么,也才能在框架不适用或过度设计时,有能力用最原生、最>直接的方式解决问题。

拥有选择权: ginkgo 的 BDD 风格虽然好,但并非所有团队都习惯或喜欢这种风格。很多Go团队更推崇原生、简洁的测试方式。学会了原生测试,>你才能根据团队情况做出明智的选择。

所以,课程的目的是面向广大开发者,而不是某个专属的测试框架。是让你成为一个能造渔具、也能用好渔具的测试高手,而不仅仅是会吃某一种鱼
的开发者。当你掌握了核心原理,无论将来遇到什么测试框架,都能快速上手,运用自如。