标签 goroutine 下的文章

一个字符引发的30%性能下降:Go值接收者的隐藏成本与优化

本文永久链接 – https://tonybai.com/2025/04/25/hidden-costs-of-go-value-receiver

大家好,我是Tony Bai。

在软件开发的世界里,细节决定成败,这句话在以简洁著称的Go语言中同样适用,甚至有时会以更出人意料的方式体现出来。

想象一下这个场景:你正在对一个稳定的Go项目进行一次看似无害的“无操作(no-op)”重构,目标只是为了封装一些实现细节,提高代码的可维护性。然而,提交代码后,CI系统却亮起了刺眼的红灯——某个核心基准测试(比如 sysbench)的性能竟然骤降了30%


(图片来源:Dolt博客原文)

这可不是什么虚构的故事,而是最近发生在Dolt(一个我长期关注的一个Go编写的带版本控制的SQL数据库)项目中的真实“性能血案”。一次旨在改进封装的重构,却意外触发了严重的性能衰退。

经过一番追踪和性能分析(Profiling),罪魁祸首竟然隐藏在代码中一个极其微小的改动里。今天,我们就来解剖这个案例,看看Go语言的内存分配机制,特别是值接收者(Value Receiver),是如何在这个过程中悄无声息地埋下性能地雷的。

案发现场:代码的前后对比

这次重构涉及一个名为 ImmutableValue 的类型,它大致包含了一个内容的哈希地址 (Addr)、一个可选的缓存字节切片 (Buf),以及一个能根据哈希解析出数据的ValueStore接口。其核心方法 GetBytes 用于获取数据,如果缓存为空,则通过 ValueStore 加载。

重构的目标是将ValueStore的部分实现细节移入接口方法ReadBytes中。

重构前的简化代码:

// (ImmutableValue 的定义和部分字段省略)

func (t *ImmutableValue) GetBytes(ctx context.Context) ([]byte, error) {
  if t.Buf == nil {
      // 直接调用内部的 load 方法填充 t.Buf
      err := t.load(ctx)
      if err != nil {
          return nil, err
      }
  }
  return t.Buf[:], nil
}

func (t *ImmutableValue) load(ctx context.Context) error {
  // ... (省略部分检查)
  // 假设 valueStore 是 t 的一个字段,类型是 nodeStore 或类似具体类型
  t.valueStore.WalkNodes(ctx, t.Addr, func(ctx context.Context, n Node) error {
        if n.IsLeaf() {
            // 直接 append 到 t.Buf
            t.Buf = append(t.Buf, n.GetValue(0)...)
        }
        return nil // 简化错误处理
  })
  return nil
}

重构后的简化代码:

// (ImmutableValue 定义同上)

func (t *ImmutableValue) GetBytes(ctx context.Context) ([]byte, error) {
    if t.Buf == nil {
        if t.Addr.IsEmpty() {
            t.Buf = []byte{}
            return t.Buf, nil
        }
        // 通过 ValueStore 接口的 ReadBytes 方法获取数据
        buf, err := t.valueStore.ReadBytes(ctx, t.Addr)
        if err != nil {
            return nil, err
        }
        t.Buf = buf // 将获取到的 buf 赋值给 t.Buf
    }
    return t.Buf, nil
}

// ---- ValueStore 接口的实现 ----

// 假设 nodeStore 是 ValueStore 的一个实现
type nodeStore struct {
  chunkStore interface { // 假设 chunkStore 是另一个接口或类型
    WalkNodes(ctx context.Context, h hash.Hash, cb CallbackFunc) error
  }
  // ... 其他字段
}

// 注意这里的接收者类型是 nodeStore (值类型)
func (vs nodeStore) ReadBytes(ctx context.Context, h hash.Hash) (result []byte, err error) {
    err = vs.chunkStore.WalkNodes(ctx, h, func(ctx context.Context, n Node) error {
        if n.IsLeaf() {
            // append 到局部变量 result
            result = append(result, n.GetValue(0)...)
        }
        return nil // 简化错误处理
    })
    return result, err
}

// 确保 nodeStore 实现了 ValueStore 接口
var _ ValueStore = nodeStore{} // 注意这里用的是值类型

代码逻辑看起来几乎没变,只是将原来load方法中的 WalkNodes 调用和 append 逻辑封装到了 nodeStore 的 ReadBytes 方法中。

然而,性能分析(Profiling)结果显示,在新的实现中,ReadBytes 方法耗费了大量时间(约 1/3 的运行时)在调用 runtime.newobject 上。Go老手都知道:runtime.newobject是Go用于在堆上分配内存的内建函数。这意味着,新的实现引入了额外的堆内存分配。

那么问题来了(这也是原文留给读者的思考题):

  • 额外的堆内存在哪里分配的?
  • 为什么这次分配发生在堆(Heap)上,而不是通常更廉价的栈(Stack)上?

到这里可能即便经验丰富的Go开发者可能也没法一下子看出端倪。如果你和我一样在当时还没想到,不妨暂停一下,仔细看看重构后的代码,特别是ReadBytes方法的定义。

当你准备好后,我们来一起揭晓答案。

破案:罪魁祸首——那个被忽略的*号

造成性能骤降的罪魁祸首,竟然只是ReadBytes方法定义中的一个字符差异!

修复方法:

diff
- func (vs nodeStore) ReadBytes(ctx context.Context, h hash.Hash) (result []byte, err error) {
+ func (vs *nodeStore) ReadBytes(ctx context.Context, h hash.Hash) (result []byte, err error) {

是的,仅仅是将 ReadBytes 方法的接收者从值类型 nodeStore 改为指针类型 *nodeStore,就挽回了那丢失的 30% 性能。

那么,这背后到底发生了什么?我们逐层剥丝去茧的看一下。

第一层:值接收者 vs 指针接收者 —— 不仅仅是语法糖

我们需要理解Go语言中方法接收者的两种形式:

  • 值接收者 (Value Receiver): func (v MyType) MethodName() {}
  • 指针接收者 (Pointer Receiver): func (p *MyType) MethodName() {}

虽然Go允许你用值类型调用指针接收者的方法(Go会自动取地址),或者用指针类型调用值接收者的方法(Go会自动解引用),但这并非没有代价

关键在于:当使用值接收者时,方法内部操作的是接收者值的一个副本(Copy)。

在我们的案例中,ReadBytes 方法使用了值接收者 (vs nodeStore)。这意味着,每次通过 t.valueStore.ReadBytes(…) 调用这个方法时(t.valueStore 是一个接口,其底层具体类型是 nodeStore),Go 运行时会创建一个 nodeStore 结构体的副本,并将这个副本传递给 ReadBytes 方法内部的vs变量。

正是这个结构体的复制操作,构成了“第一重罪”——它带来了额外的开销。

但仅仅是复制,通常还不至于引起如此大的性能问题。毕竟,Go 语言函数参数传递也是值传递(pass-by-value),复制是很常见的。问题在于,这次复制产生的开销,并不仅仅是简单的内存拷贝。

第二层:栈分配 vs 堆分配 —— 廉价与昂贵的抉择

通常情况下,函数参数、局部变量,以及这种方法接收者的副本,会被分配在栈(Stack)上。栈分配非常快速,因为只需要移动栈指针即可,并且随着函数返回,栈上的内存会自动回收,几乎没有管理成本。

但是,在某些情况下,Go 编译器(通过逃逸分析 Escape Analysis)会判断一个变量不能安全地分配在栈上,因为它可能在函数返回后仍然被引用(即“逃逸”到函数作用域之外)。这时,编译器会选择将这个变量分配在堆(Heap)上。

堆分配相比栈分配要昂贵得多:

  1. 分配本身更慢: 需要在堆内存中找到合适的空间。
  2. 需要垃圾回收(GC): 堆上的内存需要垃圾回收器来管理和释放,这会带来额外的 CPU 开销和潜在的 STW (Stop-The-World) 暂停。

在Dolt的这个案例中,性能分析工具明确告诉我们,ReadBytes 方法中出现了大量的 runtime.newobject 调用,这表明 nodeStore 的那个副本被分配到了上。

这就是“第二重罪”——本该廉价的栈上复制,变成了昂贵的堆上分配。

注:这里有些读者可能注意到了WalkNodes传入了一个闭包,闭包是在堆上分配的,但这个无论方法接收者是指针还是值,其固定开销都是存在的。不是此次“血案”的真凶。

第三层:逃逸分析的“无奈”——为何会逃逸到堆?

为什么编译器会认为 nodeStore 的副本需要分配在堆上呢?按照代码逻辑,vs 这个副本变量似乎并不会在 ReadBytes 函数返回后被引用。

原文作者使用go build -gcflags “-m” 工具(这个命令可以打印出编译器的逃逸分析和内联决策)发现,编译器给出的原因是:

store/prolly/tree/node_store.go:93:7: parameter ns leaks to {heap} with derefs=1:
  ...
  from ns.chunkStore (dot of pointer) at ...
  from ns.chunkStore.WalkNodes(ctx, ref) (call parameter) at ...
leaking param content: ns

注:这里原文也有“笔误”,代码定义用的接收者名是vs,这里逃逸分析显示的是ns。可能是后期方法接收者做了改名。

编译器认为,当 vs.chunkStore.WalkNodes(…) 被调用时,由于 chunkStore 是一个接口类型,编译器无法在编译时完全确定 WalkNodes 方法的具体实现是否会导致 vs (或者其内部字段的地址)以某种方式“逃逸”出去(比如被一个长期存活的 goroutine 捕获)。

Go 的逃逸分析虽然很智能,但并非万能。官方文档也提到它是一个“基本的逃逸分析”。当编译器不能百分之百确定一个变量不会逃逸时,为了保证内存安全(这是 Go 的最高优先级之一),它会采取保守策略,将其分配到堆上。堆分配永远是安全的(因为有 GC),尽管可能不是最高效的。

在这个案例中,接口方法调用成为了逃逸分析的“盲点”,导致编译器做出了保守的堆分配决策。

眼见为实:一个简单的复现与逃逸分析

理论讲完了,我们不妨动手实践一下,用一个极简的例子来复现并观察这个逃逸现象。

第一步:使用值接收者 (Value Receiver)

下面是模拟Dolt问题代码的示例,这里大幅做了简化。我们先用值接收者定义方法:

package main

import "fmt"

// 1. 接口
type Executor interface {
    Execute()
}

// 2. 具体实现
type SimpleExecutor struct{}

func (se SimpleExecutor) Execute() {
    // fmt.Println("Executing...") // 实际操作可以省略
}

// 3. 包含接口字段的结构体
type Container struct {
    exec Executor
}

// 4. 值接收者方法 (我们期望这里的 c 逃逸)
func (c Container) Run() {
    fmt.Println("Running via value receiver...")
    // 调用接口方法,这是触发逃逸的关键
    c.exec.Execute()
}

func main() {
    impl := SimpleExecutor{}
    cInstance := Container{exec: impl}

    // 调用值接收者方法
    cInstance.Run()

    // 确保 cInstance 被使用,防止完全优化
    _ = cInstance.exec
}

运行逃逸分析 (值接收者版本):

我们在终端中运行 go build -gcflags=”-m -l” main.go。这里关闭了内联优化,避免对结果的影响。

观察输出: 你应该会看到类似以下的行 (行号可能略有不同):

$go run -gcflags="-m -l" main.go
# command-line-arguments
./main.go:24:7: leaking param: c
./main.go:25:13: ... argument does not escape
./main.go:25:14: "Running via value receiver..." escapes to heap
./main.go:36:31: impl escapes to heap
Running via value receiver...

我们发现:leaking param: c 这条输出明确地告诉我们,Run 方法的值接收者 c(一个 Container 的副本)因为内部调用了接口方法而逃逸到了堆上。

第二步:改为指针接收者 (Pointer Receiver)

现在,我们将 Run 方法改为使用指针接收者,其他代码不变:

func (c *Container) Run() {
    fmt.Println("Running via pointer receiver...")
    c.exec.Execute()
}

再来运行逃逸分析 (指针接收者版本):

$go run -gcflags="-m -l" main.go
# command-line-arguments
./main.go:24:7: leaking param content: c
./main.go:26:13: ... argument does not escape
./main.go:26:14: "Running via pointer receiver..." escapes to heap
./main.go:36:31: impl escapes to heap
Running via pointer receiver...

对于之前的输出,两者的主要区别在于对接收者参数c的逃逸报告不同:

  • 值接收者: leaking param: c -> 接收者c的副本本身因为接口方法调用而逃逸到了堆上。
  • 指针接收者: leaking param content: c -> 接收者指针c本身并未因为接口方法调用而逃逸,但它指向或访问的内容与堆内存有关,在此例中, main函数中将具体实现赋值给接口字段时,impl会逃逸到堆(impl escapes to heap),无论接收者类型为值还是指针。

这个对比清晰地表明,使用指针接收者可以避免接收者参数本身因为在方法内部调用接口字段的方法而逃逸到堆。这通常是更优的选择,可以减少不必要的堆分配。

这个简单的重现实验清晰地印证了我们的分析:

  • 值接收者的方法内部调用了其包含的接口字段的方法时,编译器出于保守策略,可能会将值接收者的副本分配到堆上,导致额外的性能开销。
  • 而使用指针接收者时,方法传递的是指针,编译器通过指针进行接口方法的动态分发,这个过程通常不会导致接收者指针本身逃逸到堆上

小结:细节里的魔鬼与性能优化的启示

这个由一个*号引发的30%性能“血案”,给我们带来了几个深刻的启示:

  1. 值接收者有隐形成本: 每次调用都会产生接收者值的副本。虽然 Go 会自动处理值/指针的转换,但这背后是有开销的,尤其是在拷贝较大的结构体时。
  2. 拷贝可能导致堆分配: 如果编译器无法通过逃逸分析确定副本只在栈上活动(尤其是在涉及接口方法调用等复杂情况时),它就会被分配到堆上,带来显著的性能损耗(分配开销 + GC 压力)。
  3. 接口调用可能影响逃逸分析: 动态派发使得编译器难以在编译时完全分析清楚变量的生命周期,可能导致保守的堆分配决策。
  4. 优先使用指针接收者: 尤其对于体积较大的结构体,或者在性能敏感的代码路径中,使用指针接收者可以避免不必要的拷贝和潜在的堆分配,是更安全、通常也更高效的选择。当然,如果你的类型是“不可变”的,或者逻辑上确实需要操作副本,值接收者也有其用武之地,但要意识到潜在的性能影响。
  5. 善用工具: go build -gcflags “-m” 是我们理解编译器内存分配决策、发现潜在性能问题的有力武器。当遇到意外的性能问题时,检查逃逸分析的结果往往能提供关键线索。

一个小小的星号,背后却牵扯出 Go 语言关于方法接收者、内存分配和编译器优化的诸多细节。理解这些细节,正是我们写出更高性能、更优雅 Go 代码的关键。

希望这个真实的案例和简单的复现能让你对 Go 的内存管理有更深的认识。你是否也曾遇到过类似的、由微小代码改动引发的性能问题?欢迎在评论区分享你的故事和看法!

Dolt原文链接:https://www.dolthub.com/blog/2025-04-18-optimizing-heap-allocations/


今天我们深入探讨了值接收者、堆分配和逃逸分析这些相对底层的 Go 语言知识点。如果你对这些内容意犹未尽,希望:

  • 系统性地学习 Go 语言,从基础原理到并发编程,再到工程实践,构建扎实的知识体系;
  • 深入理解 Go 的设计哲学与底层实现,知其然更知其所以然;
  • 掌握更多 Go 语言的进阶技巧与避坑经验,在实践中写出更健壮、更高效的代码;

那么,我为你准备了两份“精进食粮”:

  • 极客时间专栏《Go 语言第一课》:这门课程覆盖了 Go 语言从入门到进阶所需的核心知识,包含大量底层原理讲解和实践案例,是系统学习 Go 的绝佳起点。

img{512x368}

  • 我的书籍《Go 语言精进之路》:这本书侧重于连接 Go 语言理论与一线工程实践,深入探讨了 Go 的设计哲学、关键特性、常见陷阱以及在真实项目中应用 Go 的最佳实践,助你打通进阶之路上的“任督二脉”。

img{512x368}

希望它们能成为你 Go 语言学习和精进道路上的得力助手!


商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。

世界读书日:如何高效阅读“砖头”技术书?我的心法分享(文末赠书)

本文永久链接 – https://tonybai.com/2025/04/23/tips-for-reading-technical-books

大家好,我是Tony Bai。

今天是世界读书日。聊到读书,尤其是咱们技术人经常要面对的那些厚重的“技术砖头”,估计不少朋友都有过类似的挣扎:道理都懂,书很重要,但就是感觉难啃、读不进去,或者读完就忘,效果不彰。

技术书籍往往信息密集、逻辑严谨、内容晦涩,想要高效地从中汲取养分,确实需要讲究一些方法。我自己就是一个长期主义者,坚信持续学习和深入思考的力量。多年来,我不仅坚持阅读,也一直在我的博客tonybai.com以及本公众号上进行长期的、持续的输出,这个过程让我对如何高效阅读和内化知识,有了一些切身的体会和思考。 此外,如今AI工具日益强大,如何结合传统方法与智能辅助,是一个非常值得探讨的话题。

今天,我就结合我的长期实践,和大家分享一些个人实践,特别是在攻克难点和整理笔记环节,我也会着重谈谈AI如何能成为我的得力助手,希望能帮助你更好地攻克技术“硬书”,将知识真正转化为自己的竞争力。

心法一:明确目标,精准选书——为何而读?

在信息爆炸的时代,选对书可能比努力读更重要。开始前,先明确“为何而读”:

  • 当前痛点/目标是什么? (深入Go并发?掌握K8s?学习AI Agent开发?)
  • 这本书能解决问题吗? 通过看目录、序言、书评(例如在豆瓣读书、亚马逊评论区、O’Reilly Learning Platform、Manning官网 等站点优质站点查找)、作者背景来判断。
  • 难度是否匹配? (是否需要前置知识?)

我的做法: 基于工作和学习规划、以及遇到的技术瓶颈选书,优先选择能直接解决我当下问题的、或者能为我未来方向打下坚实基础的书(这的确需要一些前瞻性的技术眼光)。带着明确的目的去读,效率和动力都会高很多。

心法二:主动出击,建立框架——如何开始?

面对“砖头书”,忌直接死磕。先做“侦察”,建立整体认知:

  • 速览目录、序言、总结: 把握全书结构、核心思想。
  • 带着问题阅读: 主动思考你想从中获得什么答案。

我的做法: 我通常会先花半小时到一小时快速“翻阅”全书,在脑海里构建一个大致的知识地图。然后根据我的目标,决定是通读全书,还是重点阅读某些章节。对于特别重要的章节,我会先看一遍小结,再带着问题去细读正文。

心法三:攻克难点,允许“跳过”

遇到难啃的概念或复杂逻辑卡壳时:

  • 别死磕,标记跳过: 保持阅读节奏,避免挫败感。后续内容或整体理解可能有助于回头解决。
  • 寻求外援: 查阅资料、社区提问,或同主题书籍的交叉阅读,从多个角度帮助理解难啃的技术概念。

AI在此环节的“神助攻”

在这个最容易卡壳、也最考验耐心的环节,AI展现出了惊人的辅助潜力,能显著提升我们攻克难点的效率。以下是一些你可以尝试的提示词示例(以经典书籍《The Go Programming Language》为例):

  • 多角度解释:

    • “请用一个现实生活中的例子,解释《The Go Programming Language》中描述的 Go channel 的概念,特别是带缓冲和不带缓冲 channel 的区别。”
    • “我正在读 TGPL 关于 interface 的章节,对于『接口值』的内部结构(类型和值)有点模糊,请用更通俗的语言解释一下,并说明为什么 nil 接口值不等于包含 nil 指针的接口值?”
    • “请对比 TGPL 中提到的 goroutine 和传统操作系统线程,用打比方的方式解释goroutine的『轻量』体现在哪里?”
  • 代码示例具象化:

    • “请根据《The Go Programming Language》中关于 select 语句的介绍,写一个简单的 Go 代码示例,展示如何使用 select 实现一个非阻塞的 channel 发送操作。”
    • “我需要理解 TGPL 中错误处理章节提到的 %w 动词,请提供一个 Go 代码片段,演示如何使用 fmt.Errorf 和 %w 来包装错误,并随后使用 errors.Is 和 errors.As 来检查和提取原始错误。”
  • 模拟对话与“抬杠”:

    • “假设你是一位 Go 语言专家,我正在学习 TGPL 的并发章节。我对于 mutex 和 channel 的选择有些困惑,在什么场景下应该优先选择 mutex?什么时候 channel 是更好的选择?我们来讨论一下,请给出你的理由和实例。”
    • “我看到 TGPL 中提到『不要通过共享内存来通信,而应该通过通信来共享内存』。这句话很经典,但我对其理解不够深入。你能挑战我的理解吗?比如,在哪些情况下共享内存(如使用 sync.Mutex)反而是更合适的选择?请举例说明。”

AI就像一位不知疲倦、拥有广阔知识的“智能私教”,能够针对你的难点进行个性化的“辅导”,极大地加速了理解和突破瓶颈的过程

心法四:提炼精华,有效笔记

“不动笔墨不读书”,关键是怎么记:

  • 用自己的话总结: 这是内化的核心,检验是否真懂。
  • 建立知识关联: 将新知识与旧知识联系起来。
  • 代码示例验证: 亲自实践代码是关键。
  • 结构化整理: 思维导图、结构化笔记等,用于复习和输出。但在我来看,这不是必须。

AI在此环节的“效率加速器”

在整理和消化大量信息的过程中,AI 同样能扮演好“智能助手”的角色,帮助我们提高效率,聚焦核心。以下是一些你可以尝试的提示词示例(同样以《The Go Programming Language》为例,前提是你拥有该书籍的电子版数据,用来喂给AI):

  • 辅助总结与提炼:

    • “请帮我将《The Go Programming Language》第七章『接口(Interfaces)』的核心内容,总结成 5-7 个关键要点,用 bullet points 形式列出。”
    • “我正在阅读 TGPL 关于『并发(Concurrency)』的部分,特别是 goroutine 和 channel。请提取这段内容中关于『select 语句』的主要用途和注意事项。”
    • (重要提示) AI 的总结是草稿,你必须用自己的理解去审核、修改、重写和完善,将信息转化为你自己的知识结构。
  • 笔记结构化建议:

    • “我正在为《The Go Programming Language》的第五章『函数(Functions)』做笔记,请给我建议 2-3 种不同的笔记组织结构,例如概念分类、按重要性排序、或者 Q&A 形式。”
  • 快速原型代码:

    • “根据 TGPL 中关于『方法(Methods)』的讨论,特别是嵌入(embedding)和方法集(method sets)的概念,请给我生成一个简单的 Go 代码示例,演示结构体嵌入后方法的调用规则。”
    • “请基于 TGPL 中对 go test 工具的介绍,给我生成一个包含基本测试函数、基准测试函数(benchmark)和示例函数(example)的简单 Go测试文件模板。”

AI在这里的作用,不是替代思考,而是将我们从一些相对重复、机械性的信息整理工作中解放出来,让我们能将宝贵的认知资源更集中地用于深度理解、批判性思考、知识关联和创造性应用上,这一点与“AI会写Go代码了,初学者还需要系统学习吗?”一文观点异曲同工。

心法五:学以致用,输出倒逼

阅读只是输入,真正的内化需要输出和实践,这是一个需要长期坚持的过程:

  • 实践应用: 在项目中应用所学知识。
  • 分享与教学: 写文章、做分享,输出是最好的学习。这也是我的实践精华。
  • 参与讨论: 与他人交流碰撞思想。
  • 持续回顾: 温故而知新。

我的做法: 我长期坚持在tonybai.com博客进行输出,这是我奉行长期主义、内化知识最重要的方式之一。 把学到的东西用自己的理解讲出来、写出来,这个过程本身就是对知识体系最好的锤炼和检验。同时,在星球里回答大家的提问,也是在不断地进行知识输出和巩固。没有输出的阅读,效果终将有限。

小结:拥抱工具,以我为主,终身学习

高效阅读技术书籍,是一项可以通过刻意练习而不断提升的技能。在 AI 时代,我们拥有了强大的工具来辅助我们攻克难关、整理信息。但请始终牢记,AI 是我们的“协处理器”和“智能拐杖”,思考和理解的主体,永远是我们自己。

找到适合自己的节奏,在关键环节善用AI的辅助,保持耐心和好奇心,将阅读视为一场需要长期投入的修行。

如果你希望将阅读和实践更紧密地结合起来,系统性地提升Go语言能力,并探索Go与AI的结合:

  • 我把我多年 Go 语言实践和思考的精华,沉淀在了 《Go语言精进之路》 这本书中,它侧重于连接理论与实践,希望能为你打通 Go 语言学习的“任督二脉”。

img{512x368}

  • 同时,在我的知识星球 「Go & AI 精进营」 中,我开设了像 【Go进阶课】 这样覆盖语法强化、设计先行与工程实践的体系化课程,并提供深度的 专家答疑 和活跃的 社区交流。我们一起学习,一起实践,一起拥抱 Go 和 AI 的未来。

img{512x368}


【世界读书日 · 特别福利】点赞 + 留言 + 在看,赢取签名版《Go语言精进之路》!

为了感谢大家一直以来的支持,并响应世界读书日的精神,鼓励大家在阅读与实践的道路上不断精进,我特别准备了一个【世界读书日专属福利】活动!参加门槛很低,大家只需移步到我的公众号同名文章下点赞 + 留言 + 在看,我将结合留言内容的质量【在看】情况,从参与本次活动的读者中,抽取1位幸运儿赠送一本由我亲笔签名《Go语言精进之路》(卷1或卷2随机)!获奖名单将在五一劳动节当天公布,获奖读者请在名单公布后的 48 小时内,主动通过公众号后台联系我,并提供准确的邮寄信息,以便我将签名版书籍寄送给您。

活动时间:即刻起 – 2025年04月30日23:59。

期待大家的踊跃参与和精彩分享! 让我们在阅读与交流中,共同进步!

希望今天分享的这些心法和 AI 应用思路能对你有所启发。你有什么高效阅读技术书籍的独门秘诀?或者你觉得 AI 在学习中还能扮演哪些角色?欢迎在评论区留言交流!


商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言进阶课 Go语言精进之路1 Go语言精进之路2 Go语言第一课 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats