Go 考古:defer 的“救赎”——从性能“原罪”到零成本的“开放编码”

本文永久链接 – https://tonybai.com/2025/10/15/go-archaeology-defer

大家好,我是Tony Bai。

在 Go 语言的所有关键字中,defer 无疑是最具特色和争议的之一。它以一种近乎“魔法”的方式,保证了资源清理逻辑的执行,极大地提升了代码的可读性和健壮性。f, _ := os.Open(“…”); defer f.Close() 这一行代码,几乎是所有 Gopher 的肌肉记忆

然而,在这份优雅的背后,曾几何时,defer 却背负着“性能杀手”的恶名。在 Go 的历史长河中,无数资深开发者,包括标准库的维护者们,都曾被迫在代码的可维护性与极致性能之间做出痛苦的抉择,含泪删掉 defer 语句,换上丑陋但高效的手动 if err != nil 清理逻辑。

你是否好奇:

  • defer 的早期实现究竟“慢”在哪里?为什么一个简单的函数调用会被放大数十倍的开销?
  • 从 Go 1.13 到 Go 1.14,Go 团队究竟施展了怎样的“魔法”,让 defer 的性能提升了超过 10 倍,几乎达到了与直接调用函数相媲美的程度?
  • 为了实现这场“性能革命”,defer 在编译器和运行时层面,经历了怎样一场从“堆分配”到“栈上开放编码(open-coded defer)”的“心脏手术”?

今天,就让我们再一次化身“Go 语言考古学家”,在Go issues以及Go团队那些著名的演讲资料中挖掘,并结合 Go 官方的设计文档,深入 defer 性能演进的“地心”,去完整地再现这场波澜壮阔的“救赎之路”。

“事后”的智慧:Defer 的设计哲学与独特性

在我们深入 defer 性能的“地心”之前,让我们先花点时间,站在一个更高的维度,欣赏一下 defer 这个语言构造本身的设计之美。defer机制 并非 Go 语言的首创,许多语言都有类似的机制来保证资源的确定性释放,但Go中defer 机制的实现方式却独树一帜,充满了 Go 语言独有的哲学。

保证“清理”的殊途同归

下面是几种主流语言的资源管理范式,这让我们能更清晰地看清 defer 的坐标:

  • C++ 的 RAII (Resource Acquisition Is Initialization):

这是一种极其强大和高效的范式。资源(如文件句柄、锁)的生命周期与一个栈上对象的生命周期绑定。当对象离开作用域时,其析构函数 (destructor) 会被编译器自动调用,从而释放资源。RAII 的优点是静态可知、零运行时开销。但它强依赖于 C++ 的析构函数和对象生命周期管理,对于一门拥有垃圾回收(GC)的语言来说,这种模式难以复制。

  • Java/Python 的 try-finally:

这是另一种常见的保证机制。finally 块中的代码,无论 try 块是正常结束还是抛出异常,都保证会被执行。try-finally 同样是静态可知的,编译器能明确地知道在每个代码块退出时需要执行什么。

这两种机制的共同点是:它们都是块级 (block-level) 的,并且清理逻辑的位置往往与资源获取的位置相距甚远

Defer 的三大独特优势

相比之下,Go 的 defer 提供了三种独特的优势,使其在代码的可读性和灵活性上脱颖而出:

  1. 就近原则,极致清晰 (Clarity):

这是 defer 最为人称道的优点。清理逻辑(defer f.Close())可以紧跟在资源获取逻辑(os.Open(…))之后。这种“开闭成对”的书写方式,极大地降低了程序员的心智负担,你再也不用在函数末尾的 finally 块和函数开头的资源申请之间来回跳转,从而有效避免了忘记释放资源的低级错误。

  1. 函数级作用域,保证完整性 (Robustness):

defer 的执行时机与函数(而非代码块)的退出绑定。这意味着,无论函数有多少个 return 语句,无论它们分布在多么复杂的 if-else 分支中,所有已注册的 defer 调用都保证会在函数返回前被执行。这对于重构和维护极其友好——你可以随意增删 return 路径,而无需担心破坏资源清理的逻辑。更重要的是,在 panic 发生时,defer 依然会被执行,这为构建健壮的、能从异常中恢复的常驻服务提供了坚实的基础。

  1. 动态与条件执行,极致灵活 (Flexibility):

这是 defer 与 RAII 和 try-finally 最本质的区别。defer 是一个完全动态的语句,它可以出现在 if 分支、甚至 for 循环中。

if useFile {
    f, err := os.Open("...")
    // ...
    defer f.Close() // 只在文件被打开时,才注册清理逻辑
}

这种条件式清理的能力,是其他静态机制难以优雅表达的。

“动态”的双刃剑

然而,defer 的动态性也是一把双刃剑。

正是因为它可以在循环中被调用,defer 在理论上可以被执行任意多次。编译器无法在编译期静态地知道一个函数到底会注册多少个 defer 调用。

这种不确定性,迫使 Go 的早期设计者必须借助运行时的帮助,通过一个动态的链表来管理 defer 调用栈。这就引出了我们即将要深入探讨的核心问题——为了这份极致的灵活性和清晰性,defer 在诞生之初,付出了怎样的性能代价?而 Go 团队又是如何通过一场载入史册的编译器革命,几乎将其“抹平”的?

现在,让我们带上“考古工具”,正式开始我们的性能探源之旅。

“原罪”:Go 1.13 之前的 defer 为何如此之慢?

在GopherCon 2020上,Google工程师Dan Scales为大家进行了一次经常的有关defer性能提升的演讲,在此次演讲中,他先为大家展示了一张令人震惊的性能对比图,也揭示了一个残酷的事实:在 Go 1.12 及更早的版本中,一次 defer 调用的开销高达 44 纳秒,而一次普通的函数调用仅需 1.7 纳秒,相差超过 25 倍

这巨大的开销从何而来?答案隐藏在早期的实现机制中:一切 defer 都需要运行时(runtime)的深度参与,并且都涉及堆分配(heap allocation)。

让我们通过 Go 团队的内部视角,来还原一下当时 defer 的工作流程:

  1. 创建 _defer 记录: 每当你的代码执行一个 defer 语句时,编译器会生成代码,在堆上分配一个 _defer 结构体。这个结构体就像一张“任务卡”,记录了要调用的函数指针、所有参数的拷贝,以及一个指向下一个 _defer 记录的指针。

  1. deferproc 运行时调用: 创建好“任务卡”后,程序会调用运行时的 runtime.deferproc 函数。这个函数负责将这张新的“任务卡”挂载到当前 goroutine 的一个链表上。这个链表,我们称之为“defer 链”。

  1. deferreturn 运行时调用: 当函数准备退出时(无论是正常 return 还是 panic),编译器会插入一个对 runtime.deferreturn 的调用。这个函数会像“工头”一样,从 defer 链的尾部开始(后进先出 LIFO),依次取出“任务卡”,并执行其中记录的函数调用。

看到了吗?每一次 defer,都至少包含:

  • 一次堆内存分配(创建 _defer 记录)。
  • 两次到运行时的函数调用 (deferproc 和 deferreturn)。

堆分配本身就是昂贵的操作,因为它需要加锁并与垃圾回收器(GC)打交道。而频繁地在用户代码和 runtime 之间切换,也带来了额外的开销。正是这“三座大山”,让 defer 在高性能场景下变得不堪重负。

Go 1.13 迈出了优化的第一步:对于不在循环中的 defer,编译器尝试将 _defer 记录分配在栈上。这避免了堆分配和 GC 的压力,使得 defer 的开销从 44ns 降低到了 32ns。这是一个显著的进步,但离“零成本”的目标还相去甚甚远。defer 依然需要与 runtime 交互,依然需要构建那个链表。

“革命”:Go 1.14 的 Open-Coded Defer

Go 1.14 带来的,不是改良,而是一场彻底的革命。Dan Scales 和他的同事们提出并实现了一个全新的机制,名为 “开放编码的 defer (Open-Coded Defer)”。

其核心思想是:对于那些简单的、非循环内的 defer,我们能不能彻底摆脱 runtime,让编译器直接在函数内生成所有清理逻辑?

答案是肯定的。这场“革命”分为两大战役:

战役一:在函数退出点直接生成代码

编译器不再生成对 deferproc 的调用。取而代之的是:

  1. 栈上“专属”空间: 在函数的栈帧(stack frame)中,为每个 defer 调用的函数指针和参数预留“专属”的存储位置。
  2. 位掩码(Bitmask): 同样在栈上,引入一个 _deferBits 字节。它的每一个 bit 位对应一个 defer 语句。当一个 defer 被执行时,不再是创建 _defer 记录,而是简单地将 _deferBits 中对应的 bit 位置为 1。这是一个极快、极轻量的操作。

当函数准备退出时,编译器也不再调用 deferreturn。它会在每一个 return 语句前,插入一段“开放编码”的清理逻辑。这段逻辑就像一个智能的“清理机器人”,它会逆序检查 _deferBits 的每一位。如果 bit 位为 1,就从栈上的“专属空间”中取出函数指针和参数,直接发起调用:

看到了吗?在正常执行路径下,整个过程没有任何堆分配,没有任何 runtime 调用!defer 的成本,被降低到了几次内存写入(保存参数和设置 bit 位)和几次 if 判断。这使得其开销从 Go 1.13 的 32ns 骤降到了惊人的 3ns,与直接调用函数(1.7ns)的开销几乎在同一个数量级!

战役二:与 panic 流程的“深度整合”

你可能会问:既然没有 _defer 链表了,当 panic 发生时,runtime 怎么知道要执行哪些 defer 呢?

这正是 Open-Coded Defer 设计中最精妙、也最复杂的部分。Go 团队通过一种名为 funcdata 的机制,在编译后的二进制文件中,为每个使用了 Open-Coded Defer 的函数,都附上了一份“藏宝图”。

这份“藏宝图”告诉 runtime:

  • 这个函数使用了开放编码。
  • _deferBits 存储在栈帧的哪个偏移量上。
  • 每个 defer 调用的函数指针和参数,分别存储在栈帧的哪些偏移量上。

当 panic 发生时,runtime 的 gopanic 函数会扫描 goroutine 的栈。当它发现一个带有 Open-Coded Defer 的栈帧时,它就会:

  1. 读取这份“藏宝图” (funcdata)。
  2. 根据“藏宝图”的指引,在栈帧中找到 _deferBits。
  3. 根据 _deferBits 的值,再从栈帧中找到并执行所有已激活的 defer 调用。

这个设计,巧妙地将 defer 的信息编码在了栈帧和二进制文件中,使得 panic 流程依然能够正确地、逆序地执行所有 defer,同时保证了正常执行路径的极致性能。

下面是Dan Scales给出的一个defer性能对比结果:

我们看到:采用Open-coded defer进行优化后,defer的开销非常接近与普通的函数调用了(1.x倍)。

小结:“救赎”的完成与新的约定

defer 的性能“救赎之路”,从 Go 1.12 的 44ns,到 Go 1.13 的 32ns(栈分配 _defer 记录),再到 Go 1.14 的 3ns(Open-Coded Defer),其演进历程波澜壮阔,是 Go 团队追求极致性能与工程实用性的最佳例证。

下面是汇总后的各个Go版本的defer实现机制与开销数据:

这场“革命”之后,Dan Scales 在演讲的最后发出了强有力的呼吁,这也应该成为我们所有 Gopher 的新共识:

defers should now be used whenever it makes sense to make code clearer and more maintainable. defer should definitely not be avoided for performance reasons.
(现在,只要能让代码更清晰、更易于维护,就应该使用 defer。绝对不应该再因为性能原因而避免使用 defer。)

defer 的“原罪”已被救赎。从现在开始,请放心地使用它,去编写更优雅、更健壮的 Go 代码吧。

参考资料

  • Proposal: Low-cost defers through inline code, and extra funcdata to manage the panic case – https://go.googlesource.com/proposal/+/master/design/34481-opencoded-defers.md
  • GopherCon 2020: Implementing Faster Defers by Dan Scales – https://www.youtube.com/watch?v=DHVeUsrKcbM
  • cmd/compile: allocate some defers in stack frames – https://github.com/golang/go/issues/6980

你的Go技能,是否也卡在了“熟练”到“精通”的瓶颈期?

  • 想写出更地道、更健壮的Go代码,却总在细节上踩坑?
  • 渴望提升软件设计能力,驾驭复杂Go项目却缺乏章法?
  • 想打造生产级的Go服务,却在工程化实践中屡屡受挫?

继《Go语言第一课》后,我的《Go语言进阶课》终于在极客时间与大家见面了!

我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造!30+讲硬核内容,带你夯实语法认知,提升设计思维,锻造工程实践能力,更有实战项目串讲。

目标只有一个:助你完成从“Go熟练工”到“Go专家”的蜕变! 现在就加入,让你的Go技能再上一个新台阶!


想系统学习Go,构建扎实的知识体系?

我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏,内容全面升级,同步至Go 1.24。首发期有专属五折优惠,不到40元即可入手,扫码即可拥有这本300页的Go语言入门宝典,即刻开启你的Go语言高效学习之旅!


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

string 与 rune 的设计哲学:为什么Go 程序员很少为“乱码”烦恼?

本文永久链接 – https://tonybai.com/2025/10/13/string-and-rune-in-go

大家好,我是Tony Bai。

“为什么我的字符又乱码了?!”

这是一个在软件开发历史上,曾让无数程序员彻夜难眠的哀嚎。处理文本,是编程中最基础的任务之一,但其背后关于编码 (Encoding) 和字符集 (Character Set) 的水,远比看起来要深。正如 Joel Spolsky 在其经典文章中疾呼的那样,这是每位软件开发者都必须了解的“绝对最低限度”的知识。

幸运的是,作为 Go 开发者,我们站在了巨人的肩膀上。Go 语言在设计之初,就以一种“独断”而富有远见的方式,为我们解决了大部分历史遗留的编码难题。然而,理解其背后的设计哲学,特别是 string 与 rune 这对“双子星”的共舞,依然是区分一名普通 Gopher 与一名优秀 Gopher 的关键。

本文将带你重温编码的基础,并深入探讨 Go 是如何从语言设计的根源上,让我们得以优雅地驰骋于多语言文本的世界。

回到本源——计算机不认识“字符”,只认识“比特”

让我们先直面一个最基本、但又常常被遗忘的事实:计算机的世界里没有字母、数字或符号,只有比特 (bit)——0 和 1 的序列。计算机所能存储和处理的一切,无论是文本、图片还是声音,最终都必须被翻译成这种二进制形式。为了让这些比特串代表人类可读的文本,我们需要一套规则,这套规则就是编码 (Encoding)

我们可以将这个过程拆分为两个核心概念:

  • 字符集 (Character Set):一个抽象的符号集合。例如,ASCII 字符集包含了 128 个字符,包括大小写英文字母、0-9 的数字、以及各种标点和控制符号。你可以把它想象成一本“字典”,里面列出了所有“合法”的字符。

  • 编码 (Encoding):一套将字符集中的每个符号,映射为特定比特序列的具体规则。例如,在 ASCII 编码中,这本“字典”规定了字母 A 对应的“页码”是 65,而 A 在计算机中的比特表示就是 65 的二进制形式 01000001。

乱码问题的根源,就在于使用了错误的“字典”和“编码规则”去解读一段比特序列。想象一下,一段用 Shift-JIS (一种日语编码) 写入的比特流,如果被错误地用 Mac Roman (一种西欧编码) 的“字典”来查找,结果自然是一堆无法理解的“天书”,也就是我们俗称的“乱码”。

Go 的“独断”——拥抱 Unicode 与 UTF-8

在 Go 诞生之前,软件世界是一片混乱的“编码战国时代”。ASCII 只有 128 个字符,连欧洲语言中常见的 é 或 ü 都无法表示。为了解决这个问题,各种各样的编码方案如雨后春笋般涌现:西欧有 ISO-8859-1,中国大陆有 GB-2312、GBK以及GB18030,中国台湾省有 BIG-5…… 每种编码都定义了自己的字符集和规则,彼此之间互不兼容。

最终,为了“书同文,车同轨”,Unicode 应运而生。它旨在创建一个包罗万象的“超级字符集”,为世界上每一种语言的每一个字符都分配一个唯一的数字编号,这个编号被称为码点 (Code Point)

然而,Unicode 本身并不是一种编码,它只是一本巨大的“字典”。如何将这些码点(数字)高效地转换为比特序列,则是由 UTF (Unicode Transformation Format) 家族的编码方案来完成的,其中最著名的就是 UTF-8

Go 的设计者们,在面对这段混乱的历史时,做出了一个极其重要的、带有“独断”色彩的决定:将 UTF-8 作为 Go 语言生态的默认和核心编码

这个决定,体现在 Go 语言的每一个角落:

  • Go 源码文件被规定必须以 UTF-8 编码保存。
  • Go 的 string 类型被设计为不可变的字节序列,并且标准库中的绝大多数操作,都假定并优化这些字节是合法的 UTF-8 编码。

这与许多早期语言(比如 PHP 等)形成了鲜明对比,在那些语言中,字符串仅仅是“字节袋”,语言本身对其内部编码一无所知,将处理编码的复杂性完全推给了开发者。Go 的这个设计,从源头上为开发者扫清了最大的障碍。

string 与 rune 的设计哲学——字节与字符的清晰分离

Go 语言为了优雅地处理 Unicode,其核心设计哲学就是清晰地分离“字节”和“字符”这两个概念,并通过 string 和 rune 这两个核心类型来体现。理解它们的区别,是掌握 Go 文本处理的关键。

  • string:一个 string 的表示是一个只读的字节切片 ([]byte)。它存储的是文本的 UTF-8 编码后的字节序列。它是数据的物理表示
  • rune:rune 是 Go 中用来代表一个 Unicode 码点 (Code Point) 的类型,它是 int32 的一个别名。你可以把它理解为 Go 世界中真正的“字符”。它是文本的逻辑表示

这种底层设计,直接导致了 len() 和 for range 在处理字符串时,那令人“困惑”却又合乎逻辑的不同行为。

一个示例,揭示所有秘密

让我们用一个包含中英文的字符串来做个实验,看看 Go 的设计哲学在实践中如何体现:

// https://go.dev/play/p/TANnV9NTQi0
package main

import (
    "fmt"
    "unicode/utf8"
)

func main() {
    s := "你好, Go"

    // --- len():返回字节的数量 ---
    // 它的操作对象是 string 的物理表示 (bytes)。
    // 在 UTF-8 中,一个英文字符占 1 个字节,一个中文字符占 3 个字节。
    // 所以,字节总数 = 2*3 (你好) + 1 (,) + 1 ( ) + 2*1 (Go) = 10
    fmt.Printf("len(s) -> The number of bytes: %d\n", len(s))

    // --- utf8.RuneCountInString():返回字符(码点)的数量 ---
    // 它的操作对象是 string 的逻辑表示 (runes)。
    // "你好, Go" 共有 6 个字符(码点)。
    fmt.Printf("Rune count -> The number of characters: %d\n", utf8.RuneCountInString(s))

    fmt.Println("\n--- Iterating with standard for loop (by byte) ---")
    // --- 传统的 for 循环:按字节遍历 ---
    // 这会逐一打印出字符串的 10 个字节。对于多字节字符,会产生乱码。
    // 这种遍历方式在处理纯 ASCII 时是正确的,但在处理 Unicode 时是错误的。
    for i := 0; i < len(s); i++ {
        fmt.Printf("%x ", s[i])
    }
    fmt.Println()

    fmt.Println("\n--- Iterating with for range (by rune) ---")
    // --- for range 循环:Go 的魔法所在,按 rune 遍历 ---
    // for range 会自动解码 UTF-8 序列,每次迭代返回一个 rune 及其起始字节的索引。
    // 这是在 Go 中遍历字符串内容的“正确”且地道的方式。
    for index, r := range s {
        fmt.Printf("index: %d, char: %c, bytes: %d\n", index, r, utf8.RuneLen(r))
    }
}

运行该示例输出如下结果:

len(s) -> The number of bytes: 10
Rune count -> The number of characters: 6

--- Iterating with standard for loop (by byte) ---
e4 bd a0 e5 a5 bd 2c 20 47 6f 

--- Iterating with for range (by rune) ---
index: 0, char: 你, bytes: 3
index: 3, char: 好, bytes: 3
index: 6, char: ,, bytes: 1
index: 7, char:  , bytes: 1
index: 8, char: G, bytes: 1
index: 9, char: o, bytes: 1

这个例子清晰地告诉我们 Go 的设计哲学:

  • len(s) 给你的是字节长度,适用于网络传输、内存分配、缓冲区大小计算等底层、面向物理的场景。
  • for i := range s 给你的是字符 (rune),适用于所有需要处理文本内容的、面向逻辑的业务场景。

这种对“字节”和“字符”的明确区分,是 Go 程序在处理多语言文本时如此健壮的根本原因。

Go 开发者的日常:实践中的编码意识

尽管 Go 为我们做了很多,但在与外部世界交互时,编码意识依然不可或缺。

  • 文件 I/O:当你从一个文件中 io.Read 时,你读到的是原始的字节流。如果这个文件不是 UTF-8 编码的(例如,一个 GBK 编码的 .txt 文件),你必须使用像 golang.org/x/text/encoding 这样的包,将其显式地转换为 UTF-8 字符串后,才能在 Go 程序中安全地处理。
import (
    "golang.org/x/text/encoding/simplifiedchinese"
    "golang.org/x/text/transform"
)

// gb_reader 是一个读取 GBK 编码文件的 io.Reader
// utf8_reader 将会是一个在读取时自动转换为 UTF-8 的 io.Reader
utf8_reader := transform.NewReader(gbk_reader, simplifiedchinese.GBK.NewDecoder())

// 从 utf8_reader 中读取的数据现在可以安全地在 Go 中使用了
utf8Bytes, _ := io.ReadAll(utf8_reader)
s := string(utf8Bytes)
  • Web 开发:在处理 HTTP 请求和响应时,Content-Type 头中的 charset=utf-8 是你与客户端之间的“契约”。Go 的 net/http 库默认会很好地处理 UTF-8,但你需要确保所有与之交互的系统都遵守了这个契约。

  • 数据库交互:一个经典的“伪正常”陷阱是,应用程序以 UTF-8 方式与数据库通信,但数据库连接或表本身却被错误地设置为 latin1 等编码。由于 latin1 是单字节编码,它可以“吞下”任何字节序列。数据存入时看似正常,应用程序读出时也能正确解析回 UTF-8 字符串。但只要你通过数据库管理工具查看,或者在数据库层面进行排序、搜索,就会立刻看到乱码。确保你的数据库连接 DSN 中明确指定了 charset=utf8mb4,是至关重要的最佳实践。

小结:站在巨人的肩膀上

Go 语言的设计,让我们不必再像前人那样,在各种编码的泥潭中苦苦挣扎。它通过将 UTF-8 提升为事实标准,并提供 string (字节序列) 和 rune (字符) 这一对强大而清晰的抽象,为我们构建了一个默认安全的文本处理世界。

此外,理解Go中 len() 与 for range在操作 string 类型数据时的区别,不仅仅是掌握一个语言的“奇技淫巧”,更是洞察 Go 语言如何从根本上解决了困扰软件行业数十年的编码难题。这份与生俱来的编码优势,正是 Go 语言简约而不简单的一个最佳例证。

如果想了解更多关于Go string和rune的“秘密”,可以进一步阅读我的《Go语言进阶课》的05讲。在《Go语言第一课》专栏的第13讲中,也有关于码点以及UTF-8编码的更为详细的讲解。

参考资料

  • What Every Programmer Absolutely, Positively Needs To Know About Encodings And Character Sets To Work With Text – https://kunststube.net/encoding/
  • The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!) – https://www.joelonsoftware.com/2003/10/08/the-absolute-minimum-every-software-developer-absolutely-positively-must-know-about-unicode-and-character-sets-no-excuses/

你的Go技能,是否也卡在了“熟练”到“精通”的瓶颈期?

  • 想写出更地道、更健壮的Go代码,却总在细节上踩坑?
  • 渴望提升软件设计能力,驾驭复杂Go项目却缺乏章法?
  • 想打造生产级的Go服务,却在工程化实践中屡屡受挫?

继《Go语言第一课》后,我的《Go语言进阶课》终于在极客时间与大家见面了!

我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造!30+讲硬核内容,带你夯实语法认知,提升设计思维,锻造工程实践能力,更有实战项目串讲。

目标只有一个:助你完成从“Go熟练工”到“Go专家”的蜕变! 现在就加入,让你的Go技能再上一个新台阶!


想系统学习Go,构建扎实的知识体系?

我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏,内容全面升级,同步至Go 1.24。首发期有专属五折优惠,不到40元即可入手,扫码即可拥有这本300页的Go语言入门宝典,即刻开启你的Go语言高效学习之旅!


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

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! 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