2025-06-22 07:22:29
本文永久链接 – https://tonybai.com/2025/06/22/unexpected-security-footguns-in-go-parsers
大家好,我是Tony Bai。
在 Go 语言中,标准库的 encoding/json 包无疑是我们日常打交道最多的伙伴之一。它简洁易用,性能尚可,支撑了无数 Go 应用的数据交换需求。然而,正如俗话所说,“最熟悉的地方可能藏着最深的坑”,最近拜读了知名安全公司 Trail of Bits 的一篇深度剖析文章——“Unexpected security footguns in Go’s parsers”(Go 解析器中意想不到的安全“绊脚石”)——让我对这个朝夕相处的伙伴有了全新的、甚至可以说是“惊出一身冷汗”的认识。
这篇文章系统性地揭示了 Go 标准库中的 JSON、XML(以及流行的第三方 YAML)解析器在处理非受信数据时,存在一些设计上或默认行为上的“特性”,这些“特性”在特定场景下很容易被攻击者利用,演变成严重的安全漏洞。文中提到的真实案例,如 Hashicorp Vault 的认证绕过 (CVE-2020-16250),更是触目惊心。
今天,我们就结合 Trail of Bits 的这篇“檄文”,深入挖掘一下 Go 解析器(特别是我们最常用的 encoding/json)的那些“隐秘角落”,看看它们是如何成为安全陷阱的,并展望一下被寄予厚望的 JSONv2 将如何带来“救赎”。
Trail of Bits 的文章通过三个核心的攻击场景,向我们展示了 Go 解析器的一些“意外行为”是如何被利用的。让我们聚焦于与 encoding/json (v1 版本,即我们目前广泛使用的版本) 相关的几个关键点:
你以为你很好地控制了哪些数据该公开,哪些该保密?但encoding/json 的一些默认行为可能会让你大吃一惊。
Go 结构体中,如果一个字段没有 json 标签,encoding/json 在反序列化时会尝试使用该字段的导出名(首字母大写)作为 JSON 键进行匹配(大小写不敏感)。这可能导致开发者预期之外的数据被修改。
// https://go.dev/play/p/soIQPrr0GiI
package main
import (
"encoding/json"
"fmt"
)
type UserNoTag struct {
Username string // 没有 json 标签,但字段名是 Username
IsAdmin bool // 同样没有标签
}
func main() {
jsonData := {"Username": "attacker", "IsAdmin": true}
var u UserNoTag
err := json.Unmarshal([]byte(jsonData), &u)
if err != nil {
fmt.Println("Error:", err)
return
}
// 预期:可能希望 IsAdmin 不被外部设置
// 结果:u.IsAdmin 会被设置为 true
fmt.Printf("User: %+v\n", u) // Output: User: {Username:attacker IsAdmin:true}
}
在这个例子中,即使 IsAdmin 字段没有 json 标签,攻击者仍然可以通过提供名为 “IsAdmin” (或 “isAdmin”, “isadmin” 等) 的 JSON 键来设置其值。如果 IsAdmin 是一个敏感字段,这就构成了一个潜在的安全风险。Trail of Bits 指出,一个分心或经验不足的开发者可能就此引入漏洞。
json:”-” 标签的正确含义是“在序列化和反序列化时完全忽略此字段”。但如果错误地与 omitempty 组合成 json:”-,omitempty”,Go 解析器会将其解释为:此字段在 JSON 中的名称是 “-” (一个短横线字符串),并且当其为空值时在序列化时省略。这意味着,它不再被忽略,而是可以通过名为 “-” 的 JSON 键来操作。看下面示例:
// https://go.dev/play/p/hmADZWNxk2Y
package main
import (
"encoding/json"
"fmt"
)
type UserMisuseDash struct {
Username string json:"username"
IsAdmin bool json:"-,omitempty" // 错误用法!
}
func main() {
// 攻击者尝试通过名为 "-" 的键设置 IsAdmin
jsonData := {"username": "guest", "-": true}
var u UserMisuseDash
err := json.Unmarshal([]byte(jsonData), &u)
if err != nil {
fmt.Println("Error:", err)
return
}
// 结果:u.IsAdmin 被成功设置为 true!
fmt.Printf("User: %+v\n", u) // Output: User: {Username:guest IsAdmin:true}
}
Trail of Bits 发现 Flipt 和 Langchaingo 等项目中都曾出现过这种误用,导致敏感字段可被外部控制。正确的忽略方式应该是 json:”-”。
这是一个更直接的错误:开发者本意是想为字段添加 omitempty 选项,却错误地将其写成了 JSON 键名。
// https://go.dev/play/p/FpH2Ff0pXZ6
package main
import (
"encoding/json"
"fmt"
)
type UserMisuseOmitempty struct {
Username string json:"username"
Role string json:"omitempty" // 错误!Role 字段在 JSON 中的名字变成了 "omitempty"
}
func main() {
jsonData := {"username": "user1", "omitempty": "admin"}
var u UserMisuseOmitempty
err := json.Unmarshal([]byte(jsonData), &u)
if err != nil {
fmt.Println("Error:", err)
return
}
// 结果:u.Role 被设置为 "admin"
fmt.Printf("User: %+v\n", u) // Output: User: {Username:user1 Role:admin}
}
Trail of Bits 在 GitHub 上搜索发现了多个知名项目(如 Gitea, Kustomize, Btcd, Evcc)中存在将字段 JSON 名错误设置为 omitempty 的情况。正确的做法应该是 json:”fieldName,omitempty” 或者如果想用默认字段名则是 json:”,omitempty”。
当同一个 JSON 数据被多个行为不一致的解析器处理时,攻击者可以利用这些差异性来绕过安全控制。
// https://go.dev/play/p/uw0ElbJYrp9
package main
import (
"encoding/json"
"fmt"
)
type ActionRequest struct {
Action string json:"action"
}
func main() {
jsonData := {"action": "readData", "action": "deleteData"}
var req ActionRequest
err := json.Unmarshal([]byte(jsonData), &req)
if err != nil {
fmt.Println("Error:", err)
return
}
// Go 会取最后一个 "action" 的值
fmt.Printf("Request: %+v\n", req) // Output: Request: {Action:deleteData}
}
如果一个权限校验服务(可能用其他语言实现,或用了取第一个值的 Go JSON 库如 jsonparser)看到的是 “readData” 并放行,而实际执行业务逻辑的 Go 服务看到的是 “deleteData”,就可能导致权限绕过。
// https://go.dev/play/p/qaQlNq4bumo
package main
import (
"encoding/json"
"fmt"
)
type Config struct {
IsEnabled bool json:"isEnabled"
}
func main() {
jsonData := {"isenabled": true} // JSON 中键名是全小写
var cfg Config
err := json.Unmarshal([]byte(jsonData), &cfg)
if err != nil {
fmt.Println("Error:", err)
return
}
// 即使大小写不匹配,v1 版本的 encoding/json 也会成功赋值
fmt.Printf("Config: %+v\n", cfg) // Output: Config: {IsEnabled:true}
// 更危险的场景,结合重复键
jsonDataAttack := {"isEnabled": false, "isenabled": true}
var cfgAttack Config
json.Unmarshal([]byte(jsonDataAttack), &cfgAttack)
// 结果可能是 true,取决于最后一个匹配上的键 (isenabled)
fmt.Printf("Attack Config: %+v\n", cfgAttack) // Output: Attack Config: {IsEnabled:true}
}
Trail of Bits 强调这是 Go JSON 解析器最关键的缺陷之一,因为它与几乎所有其他主流语言的 JSON 解析器行为都不同(它们通常是严格大小写敏感的)。攻击者可以轻易构造 payload,如 {“action”: “UserAction”, “aCtIoN”: “AdminAction”},利用这种差异性绕过权限检查。
当一个解析器被错误地用来解析另一种格式的数据,或者其对输入数据的校验不够严格时,都可能为攻击者打开方便之门。
encoding/json (v1) 默认会静默地忽略输入 JSON 中,Go 目标结构体未定义的字段。虽然在简单场景下这只是数据被丢弃,但如果应用在后续流程中使用了更通用的方式(如 map[string]interface{})来处理或透传原始 JSON 数据,这些被“忽略”的未知键就可能“复活”并造成危害。
// https://go.dev/play/p/85voViHyEEK
package main
import (
"encoding/json"
"fmt"
)
// 目标是解析成这个结构体,它没有 IsAdmin 字段
type UserProfile struct {
Username string json:"username"
Email string json:"email"
}
func processUserData(jsonData []byte) {
// 步骤 1: 尝试按预期结构体解析
var profile UserProfile
if err := json.Unmarshal(jsonData, &profile); err != nil {
fmt.Println("Error unmarshaling to UserProfile:", err)
// return
}
fmt.Printf("Parsed UserProfile: %+v\n", profile)
// 步骤 2: 假设后续流程或为了更灵活处理,
// 使用 map[string]interface{} 再次解析或直接用它承接原始数据
var rawData map[string]interface{}
if err := json.Unmarshal(jsonData, &rawData); err != nil {
fmt.Println("Error unmarshaling to map:", err)
return
}
fmt.Printf("Raw data map: %+v\n", rawData)
// 潜在风险点:如果后续逻辑不加区分地使用了 rawData 中的所有键值对
// 例如,直接将 rawData 用于更新数据库记录或传递给下游服务
if isAdmin, ok := rawData["isAdmin"].(bool); ok && isAdmin {
fmt.Println("!!! VULNERABILITY RISK: 'isAdmin' flag found in raw data and is true !!!")
// 这里可能就根据这个 isAdmin 执行了非预期的权限提升操作
}
}
func main() {
// 攻击者在 JSON 中加入了一个 UserProfile 结构体中不存在的 "isAdmin" 字段
maliciousJSON := {"username": "hacker", "email": "[email protected]", "isAdmin": true, "notes": "ignored by struct"}
fmt.Println("--- Processing Malicious Order (with unknown 'isAdmin' key) ---")
processUserData([]byte(maliciousJSON))
}
在这个例子中,json.Unmarshal 到 UserProfile 结构体时,isAdmin 和 notes 字段会被忽略。但是,当同一个 maliciousJSON 被解析到 map[string]interface{} 时,所有键(包括 isAdmin 和 notes)都会被完整地保留下来。如果后续的业务逻辑(比如权限判断、数据存储、传递给模板引擎或下游 API)不加小心地依赖了这个 rawData map,就可能错误地使用了攻击者注入的、未在预期结构体中定义的 isAdmin: true,从而导致权限提升或其他安全问题。这本质上是一种参数污染。
encoding/json (v1) 对输入数据的“纯净度”要求并非总是那么严格。json.Unmarshal通常期望输入是一个单一、完整的 JSON 值。如果JSON值后面跟着非空白的垃圾数据,它通常会报错。但是,如 Trail of Bits 指出的,json.Decoder 在处理流式数据时,如果使用其 Decode() 方法,它可能在成功解析流中的第一个有效 JSON 对象后,并不会因为流中后续存在“垃圾数据”而立即报错,而是成功返回。只有当尝试读取下一个 Token (例如调用 decoder.Token()) 并且该 Token 不是预期的 io.EOF 时,错误才会被显现。 下面Go 示例演示了 json.Decoder 对尾部垃圾数据的潜在容忍可能导致的问题:
// https://go.dev/play/p/bPTXaPHm6jD
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
)
type SimpleMessage struct {
Content string json:"content"
}
func main() {
fmt.Println("--- Testing Trailing Garbage Data with json.Decoder ---")
// 一个有效的 JSON 对象,后面跟着 "恶意payload"
jsonDataWithTrailing := {"content":"legit data"} malicious_payload_here
reader := bytes.NewReader([]byte(jsonDataWithTrailing))
decoder := json.NewDecoder(reader)
var msg SimpleMessage
// Decoder.Decode() 会尝试解码流中的下一个 JSON 值
err := decoder.Decode(&msg)
if err != nil {
// 如果 JSON 本身格式错误,这里会报错
fmt.Println("Initial Decode Error:", err)
} else {
// 第一个 JSON 对象被成功解码
fmt.Printf("Successfully Decoded Message: %+v\n", msg)
}
// 关键:检查 Decode 之后流中是否还有剩余数据
// Trail of Bits 指出这是 encoding/json 的一个开放 issue (golang/go#13407),
// 即 Decoder.Decode 后面跟非空白字符不报错。
// 通常需要额外调用 decoder.Token() 并检查是否为 io.EOF 来确保流已耗尽。
var buf [1]byte
n, errPeek := reader.Read(buf[:]) // 尝试读取 Decode 之后的数据
if n > 0 {
fmt.Printf("!!! VULNERABILITY RISK: Trailing garbage data found after valid JSON: '%s'\n", string(buf[:n]))
// 在某些场景下,如果应用只调用 Decode() 一次且不检查流的末尾,
// 攻击者可能通过附加数据来尝试进行其他类型的攻击。
} else if errPeek == io.EOF {
fmt.Println("Stream fully consumed as expected.")
} else if errPeek != nil {
fmt.Println("Error peeking after decode:", errPeek)
} else {
fmt.Println("No trailing data or EOF not reached clearly.")
}
// 更规范的检查方式是使用 decoder.More() 或尝试再解码一个Token
fmt.Println("\n--- Proper check for trailing data ---")
reader2 := bytes.NewReader([]byte(jsonDataWithTrailing))
decoder2 := json.NewDecoder(reader2)
var msg2 SimpleMessage
decoder2.Decode(&msg2) // 解码第一个
// 尝试解码下一个token,期望是EOF
tok, errTok := decoder2.Token()
if errTok == io.EOF {
fmt.Println("Proper check: Stream fully consumed (EOF).")
} else if errTok != nil {
fmt.Printf("Proper check: Error after expected JSON object: %v (Token: %v)\n", errTok, tok)
} else if tok != nil {
fmt.Printf("!!! VULNERABILITY RISK (Proper check): Unexpected token after first JSON object: %v\n", tok)
}
}
如果应用逻辑仅仅依赖 decoder.Decode() 的单次成功返回,而没有后续检查(如确保流已到达 io.EOF),攻击者就可能在有效的 JSON 数据之后附加恶意数据。这些数据可能被后续的、未预期的处理流程读取,或者在某些HTTP请求劫持、请求伪造场景中被利用。Trail of Bits 指出这是一个已知的、但因兼容性等原因未计划修复的 issue (golang/go#13407)。
虽然不是直接的 encoding/json 问题,但 Trail of Bits 强调了当数据格式处理发生混淆时(例如,用 XML 解析器去解析一个实际是 JSON 的响应),Go XML 解析器的宽松性可能导致严重问题。这提醒我们在处理任何外部输入时,都必须严格校验 Content-Type 并使用对应的正确解析器。
面对 encoding/json (v1) 的这些“隐秘角落”,Go 社区和核心团队并没有坐视不理。Trail of Bits 的文章也将最终的希望寄托在了将以实验性特性 GOEXPERIMENT=jsonv2 存在于 Go 1.25的encoding/json/v2了。
根据官方提案 (GitHub Issue #71497) ,json/v2 在安全性方面将带来诸多关键改进,很多都直接针对上述的“痛点”:
这些改进,特别是默认行为的调整,将极大地提升 Go 应用在处理不可信 JSON 数据时的安全性,从源头上减少了许多潜在的漏洞。
在 JSONv2 真正成为主流之前,我们能做些什么来保护我们的 Go 应用呢?Trail of Bits 给出了一些宝贵的建议,结合 JSONv2 的趋势,我们可以总结为:
Trail of Bits 的这篇文章为我们所有 Go 开发者敲响了警钟:即使是像 encoding/json 这样基础、常用的标准库,也可能因为一些不符合直觉的默认行为或被忽视的配置,而成为安全攻击的突破口。
理解这些“隐秘角落”,认识到“便利”与“安全”之间的权衡,并积极拥抱像 JSONv2 这样的改进,是我们构建更健壮、更安全的 Go 应用的必经之路。在日常开发中,对任何外部输入都保持一份警惕,审慎处理数据的解析与校验,应成为我们每个人的习惯。
你是否在项目中遇到过类似 Go 解析器的“坑”?你对 JSONv2 有哪些期待?欢迎在评论区分享你的经验和看法! 如果觉得本文对你有所启发,也请不吝点个【赞】和【在看】,让更多 Gopher 关注 Go 的解析器安全!
资料地址:https://blog.trailofbits.com/2025/06/17/unexpected-security-footguns-in-gos-parsers/
你的Go技能,是否也卡在了“熟练”到“精通”的瓶颈期?
继《Go语言第一课》后,我的《Go语言进阶课》终于在极客时间与大家见面了!
我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造!30+讲硬核内容,带你夯实语法认知,提升设计思维,锻造工程实践能力,更有实战项目串讲。
目标只有一个:助你完成从“Go熟练工”到“Go专家”的蜕变! 现在就加入,让你的Go技能再上一个新台阶!
商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。
© 2025, bigwhite. 版权所有.
2025-06-21 06:26:31
本文永久链接 – https://tonybai.com/2025/06/21/kubernetes-2-0
大家好,我是Tony Bai。
自 2014 年首次提交以来,Kubernetes 已走过辉煌的十年。它从一个“没人能念对名字”的希腊词汇,成长为容器编排领域无可争议的事实标准,深刻地改变了我们构建、部署和管理应用的方式。我们不再满足于在服务器层面“管理基础设施”,一切都变得声明式、可扩展、可恢复,甚至(如果你足够幸运的话)能够自我修复。
然而,正如任何伟大的技术旅程一样,Kubernetes 的发展也并非一帆风顺。尽管它带来了巨大的生产力提升,但其陡峭的学习曲线、某些领域“不够固执己见 (not opinionated enough)”导致的常见错误和配置失误、以及生态系统中持续的“变动”,仍然让许多开发者和运维者“痛并快乐着”。我们依然会踩到那些文档早已记录的“地雷”。
站在十年的重要节点,回望过去,展望未来,一个有趣的问题自然而然地浮现:如果我们有机会基于今天的认知和经验,重新构想一个 Kubernetes 2.0,它会是什么样子?我们能做哪些改变,让这个伟大的工具更普惠、更强大、更易用?
最近,一篇题为《What Would a Kubernetes 2.0 Look Like》的博文,就针对这个问题提出了一系列大胆而深刻的畅想,直指当前 K8s 生态中的核心痛点。今天,我们就来一起探讨这些引人深思的观点。
注:本文观点主要源自上述博文,并结合我个人的一些思考,希望能为大家带来启发。
在畅想未来之前,我们必须承认 Kubernetes 取得的巨大成功。它之所以能成为云原生时代的基石,离不开其核心价值:
然而,正如文章作者所言,“旅程并非没有问题”。“默认值是技术中最强大的力量 (defaults are the most powerful force in technology)”,而 Kubernetes 在某些方面的“默认”或“缺失”,恰恰是许多痛点的根源。 这正是我们畅想“K8s 2.0”的出发点——通过设定更优的“快乐路径 (happy path)”,提升整个生态的健康度和用户体验。
“YAML 之所以吸引人,是因为它既不是 JSON 也不是 XML,这就像说你的新车很棒,因为它既不是马也不是独轮车一样。” 文章作者对 YAML 的这句犀利点评,道出了许多 K8s 用户的心声。
YAML最初凭借其看似简洁的格式在 Kubernetes 中胜出,但其在实践中暴露的问题也日益突出:
文章大胆提议,Kubernetes 2.0 应该用 HCL (HashiCorp Configuration Language) 替换 YAML。 HCL 作为 Terraform 的配置语言,早已被广大云原生开发者所熟悉。其核心优势在于:
作者通过对比简单的 YAML 和 HCL 示例,直观地展示了 HCL 在类型安全和动态配置生成方面的优越性:
# YAML doesn't enforce types
replicas: "3" # String instead of integer
resources:
limits:
memory: 512 # Missing unit suffix
requests:
cpu: 0.5m # Typo in CPU unit (should be 500m)
vs.
# HCL
replicas = 3 # Explicitly an integer
resources {
limits {
memory = "512Mi" # String for memory values
}
requests {
cpu = 0.5 # Number for CPU values
}
}
尽管 HCL 可能略显冗长,且其 MPL-2.0 许可证与 K8s 的 Apache 2.0 许可证的整合需要法律审查,但作者认为,为了大幅改善配置体验,这些障碍值得克服。
etcd 作为 Kubernetes 集群状态的权威存储,一直以来都扮演着至关重要的角色。然而,文章指出,etcd 作为唯一的默认后端存储,也带来了一些局限:
因此,文章建议 Kubernetes 2.0 应该官方化 kine (k3s-io/kine) 等项目的工作,提供可插拔的后端存储抽象层。 这将允许:
此外,Go 语言在构建分布式一致性存储方面拥有优秀的库(如 hashicorp/raft,etcd 本身也是 Go 编写的)。这些技术积累能否为 Kubernetes 构建更灵活、更高效的可插拔存储后端提供更多思路?
Helm 作为 Kubernetes 事实上的包管理器,为社区贡献了标准化的应用分发和管理方式。文章作者首先感谢了 Helm 维护者的辛勤工作。但紧接着,便毫不留情地指出了 Helm 在实践中的诸多“噩梦”:
作者断言:“没有办法让 Helm 足够好地完成‘管理地球上所有关键基础设施的包管理器’这项任务。”
因此,文章畅想了一个名为 KubePkg 的 Kubernetes 原生包管理系统,其核心设计理念借鉴了成熟的 Linux 包管理系统,并充分利用了 Kubernetes CRD 的能力:
除了上述三大核心变革,文章还提出了一个颇具前瞻性的建议:Kubernetes 2.0 应将默认网络模式切换到 IPv6。
其理由在于,IPv4 带来的 NAT 穿透复杂性、IP 地址耗尽焦虑(即使在私有网络中,大规模集群也可能迅速耗尽 /20 这样的网段)等问题,已经浪费了全球开发者和运维者大量的时间和精力。
在 K8s 内部默认使用 IPv6,可以:
作者强调,这并非要求整个互联网立即切换到 IPv6,而是 Kubernetes 自身可以主动进化,以解决其在当前规模下面临的 IP 地址管理和网络复杂性问题。
“Kubernetes is an open platform, so the community can build these solutions.” (K8s 是一个开放平台,所以社区可以构建这些解决方案。)这是对类似“2.0”畅想的常见反驳。但文章作者一针见血地指出,这种说法忽略了一个关键点:“默认值是技术中最强大的力量。” 核心项目定义的“快乐路径”将主导 90% 用户的交互方式。
如果 Kubernetes 2.0 能够在配置语言、后端存储、包管理乃至网络模型这些核心体验上,提供更简洁、更安全、更强大、更易用的“默认选项”,那么整个生态系统都将因此受益。
这无疑是一份雄心勃勃的畅想清单。但正如作者所言:“如果我们打算做梦,那就做个大梦。毕竟,我们是那个认为将一项技术命名为‘Kubernetes’也能流行起来的行业,而且不知何故它确实做到了!”
Kubernetes 的第一个十年,奠定了其在云原生领域的王者地位。下一个十年,它需要在保持核心优势的同时,勇于直面和解决用户在实践中遇到的真实痛点,不断进化,提供更极致的用户体验。这些“2.0”的畅想,无论最终能否完全实现,都为我们指明了值得努力的方向。
参考文章地址:https://matduggan.com/what-would-a-kubernetes-2-0-look-like
聊一聊,也帮个忙:
欢迎在评论区留下你的真知灼见。如果你觉得这篇文章引发了你的思考,也请转发给你身边的云原生同道们,一起畅想 Kubernetes 的未来!
精进有道,更上层楼
极客时间《Go语言进阶课》上架刚好一个月,受到了各位读者的热烈欢迎和反馈。在这里感谢大家的支持。目前我们已经完成了课程模块一『语法强化篇』的 13 讲,为你系统突破 Go 语言的语法认知瓶颈,打下坚实基础。
现在,我们已经进入模块二『设计先行篇』,这不仅包括 API 设计,更涵盖了项目布局、包设计、并发设计、接口设计、错误处理设计等构建高质量 Go 代码的关键要素。
这门进阶课程,是我多年 Go 实战经验和深度思考的结晶,旨在帮助你突破瓶颈,从“会用 Go”迈向“精通 Go”,真正驾驭 Go 语言,编写出更优雅、更高效、更可靠的生产级代码!
扫描下方二维码,立即开启你的 Go 语言进阶之旅!
如果你对Go语言的底层原理和高级技巧充满好奇,渴望构建更坚实的技术壁垒,我诚挚地邀请您关注我的微专栏系列。在这里,我们拒绝浮光掠影,只做深度挖掘:
商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。
© 2025, bigwhite. 版权所有.
2025-06-20 21:51:21
本文永久链接 – https://tonybai.com/2025/06/20/redmonk-index-2025-jan
大家好,我是Tony Bai。
编程语言的江湖,总是风起云涌,新旧更迭。而 RedMonk 编程语言排行榜,以其独特的视角(结合 GitHub 的代码活跃度和 Stack Overflow 的讨论热度),长期以来都是我们观察这片江湖风向的重要参考。
就在最近,RedMonk发布了其2025年1月的编程语言排行榜。榜单本身波澜不惊,Top 20 的名单几乎与上一期如出一辙,这似乎预示着编程语言领域正进入一个相对“固化”的时期。然而,在这份看似平静的榜单背后,却潜藏着一个巨大的变量,一个足以让 RedMonk 自身都开始反思其排行方法论的“房间里的大象”——那就是 AI 的崛起,及其对 Stack Overflow 数据源的颠覆性冲击。
今天,我们就来解读这份最新的 RedMonk 排行榜,看看 Go 语言在其中表现如何,更重要的是,探讨在 AI 时代,我们该如何看待这类排行榜,以及 Go 语言的未来又将走向何方。
在解读具体排名之前,我们有必要简单回顾一下 RedMonk 排行榜的方法论。它并非统计当前“谁用得多”,而是试图通过两个维度的数据来预测语言的未来采用趋势:
RedMonk 强调,榜单的“分层 (Tiering)”比具体的数字名次更重要,因为精确排名本身就存在误差。同时,对于排名靠后的语言,由于数据量较小,其排名的波动性和不确定性会更大。
本次 2025 年 1 月的排行,最大的看点莫过于 RedMonk 博客作者 Stephen O’Grady 对 Stack Overflow (以下有时简称SO)数据有效性的公开疑虑。他明确指出,随着 ChatGPT、GitHub Copilot 等 AI 工具的普及,开发者遇到问题时,直接向 AI 提问的比例越来越高,而去 Stack Overflow 搜索或提问的需求显著下降。这导致 Stack Overflow 整体流量和特定语言标签下的讨论量都在萎缩,从而可能扭曲了基于 StackOverflow 数据的排名。RedMonk 甚至在考虑未来是否要调整 SO 数据的权重,甚至完全放弃使用它。
这无疑为我们解读本次榜单,尤其是观察那些 SO 数据占比较重的语言,提供了一个全新的、也是更具挑战性的视角。
在这样的背景下,我们来看看Go语言的表现:
RedMonk 的博文还特别点出了一些值得关注的语言动态,通过与这些语言的对比,我们可以更清晰地看到 Go 的独特价值和发展趋势。
尽管 TypeScript 在 JavaScript 生态中不可或缺,其排名也高居第 6,但博文指出它似乎进入了一个“增长平台期”,难以再向上突破。
RedMonk 提到了 TypeScript 在可扩展性 (scalability) 方面可能遇到的挑战,并直接点名了微软决定使用 Go 语言重写 TypeScript 的编译器 (tsc) 和相关工具链这一标志性事件。
当然,这无疑是对 Go 语言在构建大规模、高性能开发工具和基础设施方面能力的最好背书。当连 TypeScript 这样的语言工具自身都遇到扩展性瓶颈时,他们选择了 Go 作为解决方案。这充分证明了 Go 在工程效率、编译速度、并发处理和静态二进制部署等方面的核心优势,使其成为构建下一代开发工具(编译器、Linter、语言服务器等)的优选语言。Go,正在成为越来越多关键技术的“幕后英雄”。
这两位 JVM 生态的“优等生”排名稳定,但向上突破的动力似乎不足。Go 早已在排名上超越它们。
随着 Go 在微软等传统“非 Go”大厂中找到新的应用场景(如上述 TypeScript 工具链),以及 Rust 在对安全和性能有极致要求的服务端负载中逐渐蚕食地盘,Kotlin 和 Scala 的增长路径面临着不小的挑战。
Go 凭借其简洁的语法、高效的并发模型、出色的网络性能、以及与云原生生态的无缝集成,在现代后端服务开发领域,对传统的 JVM 语言形成了持续且强劲的竞争压力。对于追求快速迭代、高并发、低资源占用的新项目,Go 往往是更具吸引力的选择。
许多被 RedMonk 关注的新兴语言,在本次排名中大多出现了下滑,并且呈现出 GitHub 排名远好于 Stack Overflow 排名的特点。
这很可能就是前文提到的 AI 对 Stack Overflow 数据冲击的直接体现。新兴语言本身在 SO 上的讨论基数就小,当整体 SO 流量下降时,它们受到的负面影响会更加不成比例。
这再次提醒我们,在评估语言趋势时,需要警惕单一数据源(尤其是易受外部因素干扰的数据源)的局限性。Go 之所以能在榜单中保持稳定,更多是依赖其在 GitHub 上庞大且活跃的真实代码贡献和项目应用,这比社区讨论热度更能反映语言的实际生命力。
AI 代码助手(如 ChatGPT, GitHub Copilot)的普及,正在深刻改变开发者的工作习惯。遇到问题,许多人可能首先想到的是“问 AI”,而不是去 Stack Overflow 搜索或提问。这对依赖 SO 数据的 RedMonk 排行榜方法论构成了前所未有的挑战。Stephen O’Grady 的坦诚,也预示着未来编程语言趋势的观察方法可能需要革新。
在这样的背景下,Go 语言的机遇何在?
RedMonk 的最新编程语言排行榜,在 AI 席卷技术圈的当下,给我们带来了新的思考。Stack Overflow 讨论热度的“失真”,或许只是 AI 改变我们工作和学习方式的一个缩影。
对于 Go 语言而言,其在榜单中的稳定表现,特别是在 GitHub 维度上的持续强势,证明了其深厚的开发者基础和旺盛的生态活力。像微软选择用 Go 重写 TypeScript 工具链这样的行业案例,更是对其核心竞争力的有力印证。
面对 AI 带来的不确定性,Go 语言凭借其在构建高性能网络服务、云原生基础设施、以及高效开发工具等领域的明确价值定位,依然展现出强大的韧性和广阔的前景。未来,它不仅将继续作为这些领域的中流砥柱,更有望在 AI 基础设施和工程化领域扮演越来越重要的角色。
作为 Gopher,我们既要看到排行榜数据的变化,更要理解变化背后的深层逻辑。坚守 Go 语言的核心价值,持续学习和实践,同时对新技术保持开放和探索的心态,这或许才是我们在这个快速变化的时代中,最稳妥的前行之道。
你对这份 RedMonk 榜单有什么看法?AI 的出现改变了你获取技术信息的习惯吗?欢迎在评论区分享你的观点!
精进有道,更上层楼
极客时间《Go语言进阶课》上架刚好一个月,受到了各位读者的热烈欢迎和反馈。在这里感谢大家的支持。目前我们已经完成了课程模块一『语法强化篇』的 13 讲,为你系统突破 Go 语言的语法认知瓶颈,打下坚实基础。
现在,我们已经进入模块二『设计先行篇』,这不仅包括 API 设计,更涵盖了项目布局、包设计、并发设计、接口设计、错误处理设计等构建高质量 Go 代码的关键要素。
这门进阶课程,是我多年 Go 实战经验和深度思考的结晶,旨在帮助你突破瓶颈,从“会用 Go”迈向“精通 Go”,真正驾驭 Go 语言,编写出更优雅、
更高效、更可靠的生产级代码!
扫描下方二维码,立即开启你的 Go 语言进阶之旅!
感谢阅读!
商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。
© 2025, bigwhite. 版权所有.
2025-06-20 07:21:23
本文永久链接 – https://tonybai.com/2025/06/20/about-errors-join
大家好,我是Tony Bai。
错误处理,无疑是软件开发中永恒的核心议题之一。Go 语言以其独特的、显式的错误处理机制(即 error 作为普通值返回)而著称,这种设计强调了对错误的关注和及时处理。自 Go 1.13 引入错误包装 (wrapping) 机制以来,Go 的错误处理能力得到了显著增强。而在Go 1.20 版本中,标准库 errors 包更是带来了一个备受关注的新成员:errors.Join() 函数。
这个函数允许我们将多个 error 值合并成一个单一的 error 值,并且合并后的错误依然可以通过 errors.Is 和 errors.As 进行检查。一时间,社区中对其评价不一:有人称之为“天赐之物”,认为它在特定场景下能极大提升代码表达力和用户体验;也有人持审慎态度,强调应坚守“快速失败 (Fail Fast)”的原则,避免滥用错误聚合。
那么,errors.Join() 究竟是解决特定痛点的“良药”,还是可能被误用的“潘多拉魔盒”?它与 Go 一贯倡导的错误处理哲学是相辅相成,还是有所背离?今天,我们就结合社区的讨论,深入探讨 errors.Join() 的适用场景、潜在风险以及最佳实践。
在社区讨论中,有开发者盛赞 errors.Join(),认为它“在需要一次性检查多个不相关错误,或者创建类似伪堆栈跟踪结构以追踪错误传播路径的场景下,是天赐之物,非常棒!”
然而,一些资深 Go 开发者则给出了更审慎的观点:“请不要鼓吹无条件地聚合错误。遵循‘最小惊奇原则’,绝大多数情况下应该在遇到第一个错误时就‘快速失败’。合并错误的场景虽然存在,但合法地罕见。鼓励大家在假设需要合并错误之前,先思考 API 边界及其错误契约。”
这两种截然不同的看法,恰恰反映了 errors.Join() 在实践中可能带来的困惑和需要权衡的场景。
尽管“快速失败”是处理错误的主流且通常是正确的策略,但在某些特定场景下,聚合多个错误信息并一次性返回,确实能带来显著的收益。社区讨论中,开发者们也分享了他们认为 errors.Join() 非常适用的场景:
这是被提及最多的场景。当处理用户输入(如表单提交)或 API 请求参数校验时,如果每次只返回第一个发现的校验错误,用户就不得不反复提交、逐个修改,体验极差。此时,将所有校验不通过的字段错误聚合起来,一次性反馈给用户,无疑是更友好的做法。
// https://go.dev/play/p/pK6cVq9exkL
package main
import (
"errors"
"fmt"
"strings"
)
type UserRequest struct {
Username string
Email string
Password string
}
func validateRequest(req UserRequest) error {
var errs []error
if len(req.Username) < 3 {
errs = append(errs, errors.New("用户名长度不能小于3个字符"))
}
if !strings.Contains(req.Email, "@") {
errs = append(errs, errors.New("邮箱格式不正确"))
}
if len(req.Password) < 6 {
errs = append(errs, errors.New("密码长度不能小于6个字符"))
}
// 使用 errors.Join 合并所有验证错误
// errors.Join 会自动忽略 nil 错误
return errors.Join(errs...)
}
func main() {
req1 := UserRequest{"us", "email", "pass"}
if err := validateRequest(req1); err != nil {
fmt.Printf("请求1校验失败:\n%v\n", err)
// 调用方可以通过 errors.Is 或 errors.As 进一步检查具体错误类型
// 例如,如果错误是自定义类型,可以 errors.As(err, &targetErr)
}
req2 := UserRequest{"myuser", "[email protected]", "mypassword"}
if err := validateRequest(req2); err != nil {
fmt.Printf("请求2校验失败:\n%v\n", err)
} else {
fmt.Println("请求2校验通过!")
}
}
运行该示例的输出如下(对于请求1):
请求1校验失败:
用户名长度不能小于3个字符
邮箱格式不正确
密码长度不能小于6个字符
当启动多个 goroutine 执行并行操作时(例如,并发请求多个下游服务、并行处理一批数据),如果只关心第一个发生的错误,可能会丢失其他并行任务中同样重要的错误信息。此时,等待所有任务完成,收集所有可能发生的错误,并用 errors.Join() 合并,能提供更全面的错误视图。
// https://go.dev/play/p/ZtAm2-Agyo1
package main
import (
"errors"
"fmt"
"sync"
"time"
)
func processAsyncTask(id int, fail bool) error {
fmt.Printf("任务 %d 开始...\n", id)
time.Sleep(time.Duration(id*50) * time.Millisecond) // 模拟不同耗时
if fail {
fmt.Printf("任务 %d 失败!\n", id)
return fmt.Errorf("任务 %d 执行失败", id)
}
fmt.Printf("任务 %d 完成。\n", id)
return nil
}
func main() {
tasks := []bool{false, true, false, true, false} // 任务是否失败的标志
var wg sync.WaitGroup
errs := make([]error, len(tasks)) // 用于收集每个任务的错误
for i, failFlag := range tasks {
wg.Add(1)
go func(idx int, fail bool) {
defer wg.Done()
errs[idx] = processAsyncTask(idx+1, fail)
}(i, failFlag)
}
wg.Wait()
// 使用 errors.Join 合并所有任务的错误
// errors.Join 会自动过滤掉结果为 nil 的 errs[idx]
combinedErr := errors.Join(errs...)
if combinedErr != nil {
fmt.Printf("\n并行任务执行完毕,发生以下错误:\n%v\n", combinedErr)
} else {
fmt.Println("\n所有并行任务执行成功!")
}
}
运行上述代码示例,我们将得到:
任务 5 开始...
任务 4 开始...
任务 1 开始...
任务 2 开始...
任务 3 开始...
任务 1 完成。
任务 2 失败!
任务 3 完成。
任务 4 失败!
任务 5 完成。
并行任务执行完毕,发生以下错误:
任务 2 执行失败
任务 4 执行失败
在函数中,defer 语句常用于执行清理操作,如关闭文件、释放锁等。这些清理操作本身也可能返回错误。如果函数主体也返回了错误,我们就面临如何处理这两个(或多个)错误的问题。简单地忽略 defer 中的错误或用它覆盖主体错误都可能导致重要信息的丢失。errors.Join() 提供了一种优雅的方式来合并它们。
//https://go.dev/play/p/ccKUkWXMbuN
package main
import (
"errors"
"fmt"
"os"
)
func writeFileAndClose(filename string, data []byte) (err error) {
f, err := os.Create(filename)
if err != nil {
return fmt.Errorf("创建文件失败: %w", err)
}
defer func() {
// 在 defer 中调用 Close,并将其错误与函数可能已有的错误合并
closeErr := f.Close()
if closeErr != nil {
fmt.Printf("关闭文件 %s 时发生错误: %v\n", filename, closeErr)
}
// 使用 errors.Join 合并主体错误和 defer 中的错误
// 如果 err 为 nil,Join 的行为是返回 closeErr
// 如果 closeErr 为 nil,Join 的行为是返回 err
// 如果两者都非 nil,则合并
err = errors.Join(err, closeErr)
}()
_, err = f.Write(data)
if err != nil {
// 为了能被 defer 中的 Join 合并,需要将错误赋值给命名返回值 err
err = fmt.Errorf("写入文件失败: %w", err)
return // defer 会在这里执行
}
// 模拟写入成功,但关闭失败的场景
// 或者写入失败,关闭也失败的场景
return nil // 如果写入成功,defer 仍会执行关闭并可能 Join 错误
}
func main() {
// 场景1: 写入成功,关闭成功 (假设)
// (为了演示,我们不实际创建文件,避免权限问题)
fmt.Println("测试场景:写入和关闭都成功 (理想情况)")
// err := writeFileAndClose("good.txt", []byte("hello"))
// fmt.Printf("结果: %v\n\n", err) // 应为 nil
// 场景2: 模拟写入失败 (err 非 nil),关闭也可能失败 (closeErr 非 nil)
// 为了触发写入失败,我们可以尝试写入一个只读文件或无效路径
// 为了触发关闭失败,这比较难模拟,但 errors.Join 能处理这种情况
// 这里我们直接在函数逻辑中模拟这种情况
badWriteFunc := func() (err error) { // 使用命名返回值
fmt.Println("测试场景:写入失败,关闭也失败")
// 模拟写入失败
mainWriteErr := errors.New("模拟写入操作失败")
err = mainWriteErr // 赋值给命名返回值
defer func() {
simulatedCloseErr := errors.New("模拟关闭操作也失败")
fmt.Printf("关闭时发生错误: %v\n", simulatedCloseErr)
err = errors.Join(err, simulatedCloseErr) // 合并
}()
return // 返回 mainWriteErr,然后 defer 执行
}
errCombined := badWriteFunc()
if errCombined != nil {
fmt.Printf("组合错误:\n%v\n", errCombined)
// 我们可以检查这两个错误是否都存在
if errors.Is(errCombined, errors.New("模拟写入操作失败")) {
fmt.Println("包含:模拟写入操作失败")
}
if errors.Is(errCombined, errors.New("模拟关闭操作也失败")) {
fmt.Println("包含:模拟关闭操作也失败")
}
}
}
运行该示例:
测试场景:写入和关闭都成功 (理想情况)
测试场景:写入失败,关闭也失败
关闭时发生错误: 模拟关闭操作也失败
组合错误:
模拟写入操作失败
模拟关闭操作也失败
尽管 errors.Join() 在上述场景中表现出色,但我们不能忘记 Go 错误处理的一个核心原则——快速失败。 一些资深开发者在社区讨论中反复强调了这一点。
“快速失败”意味着:
在绝大多数情况下,“快速失败”是更简单、更可预测、更易于调试的错误处理策略。它符合“最小惊奇原则”,让代码的行为更符合直觉。
有开发者还提出的另一个关键点是:“在假设你需要合并错误之前,先思考你的 API 边界及其错误契约。”
一个设计良好的 API 应该清晰地告知调用者:
如果一个 API 的职责是单一且明确的,那么通常情况下,它在遇到第一个无法自行处理的错误时就应该返回,而不是试图收集所有可能的内部错误再“打包”抛给调用者。过度使用 errors.Join() 向上层传递大量不相关的细粒度错误,可能会让调用者无所适从,造成信息噪音,反而违背了 Go 错误处理的明确性原则。
结合上述讨论,以下是一些不建议或需要谨慎使用 errors.Join() 的场景:
值得注意的是,在 Go 1.20 版本中,除了引入 errors.Join() 函数外,fmt.Errorf 的 %w 动词也得到了增强,现在它支持同时包装多个错误。这为我们组合错误信息提供了另一种选择。那么,这两者在使用和行为上有什么区别呢?
我们来看一个例子:
// https://go.dev/play/p/X6aAjE0LdsY
package main
import (
"errors"
"fmt"
)
func main() {
var err1 = errors.New("错误1")
var err2 error // nil error
var err3 = errors.New("错误3")
// 使用 errors.Join
joinedErr := errors.Join(err1, err2, err3)
fmt.Printf("errors.Join 结果:\n%v\n\n", joinedErr)
// 输出会包含 err1 和 err3,err2 (nil) 会被忽略
// 使用 fmt.Errorf 包装多个错误
// 注意:如果 err2 是 nil,"%w" 会输出 "<nil>"
wrappedErr := fmt.Errorf("组合错误: 第一个: %w, 第二个(nil): %w, 第三个: %w", err1, err2, err3)
fmt.Printf("fmt.Errorf 结果:\n%v\n\n", wrappedErr)
// 演示 errors.Is 对两者的行为
fmt.Printf("errors.Is(joinedErr, err1): %t\n", errors.Is(joinedErr, err1)) // true
fmt.Printf("errors.Is(joinedErr, err2): %t\n", errors.Is(joinedErr, err2)) // false (因为 err2 是 nil 且被忽略)
fmt.Printf("errors.Is(joinedErr, err3): %t\n", errors.Is(joinedErr, err3)) // true
fmt.Printf("errors.Is(wrappedErr, err1): %t\n", errors.Is(wrappedErr, err1)) // true
// 对于 fmt.Errorf,如果被包装的 err 是 nil,errors.Is 无法通过 %w 找到它
fmt.Printf("errors.Is(wrappedErr, err2): %t\n", errors.Is(wrappedErr, err2)) // false
fmt.Printf("errors.Is(wrappedErr, err3): %t\n", errors.Is(wrappedErr, err3)) // true
// 如果所有错误都是 nil
var nilErr1, nilErr2 error
joinedNil := errors.Join(nilErr1, nilErr2)
fmt.Printf("errors.Join(nil, nil) is nil: %t\n", joinedNil == nil) // true
// fmt.Errorf 在所有 %w 都为 nil 时,如果格式化字符串本身为空,则可能返回 nil
// 但通常会包含格式化字符串本身,所以不为 nil
wrappedAllNil := fmt.Errorf("错误: %w, %w", nilErr1, nilErr2)
fmt.Printf("fmt.Errorf(\"错误: %%w, %%w\", nil, nil) is nil: %t\n", wrappedAllNil == nil) // false
}
运行示例输出如下结果:
errors.Join 结果:
错误1
错误3
fmt.Errorf 结果:
组合错误: 第一个: 错误1, 第二个(nil): %!w(<nil>), 第三个: 错误3
errors.Is(joinedErr, err1): true
errors.Is(joinedErr, err2): false
errors.Is(joinedErr, err3): true
errors.Is(wrappedErr, err1): true
errors.Is(wrappedErr, err2): false
errors.Is(wrappedErr, err3): true
errors.Join(nil, nil) is nil: true
fmt.Errorf("错误: %w, %w", nil, nil) is nil: false
// https://go.dev/play/p/8Zb2mvSFlFw
package main
import (
"errors"
"fmt"
)
type specialError struct {
msg string
}
func (e *specialError) Error() string {
return e.msg
}
func main() {
errA := errors.New("错误A")
errB := &specialError{"特殊错误B"}
errC := errors.New("错误C")
// 使用 errors.Join
joined := errors.Join(errA, errB, errC)
fmt.Println("使用 errors.Join 解包:")
if unwrap, ok := joined.(interface{ Unwrap() []error }); ok {
originalErrors := unwrap.Unwrap()
for i, e := range originalErrors {
fmt.Printf(" 原始错误 %d: %v (类型: %T)\n", i+1, e, e)
// 可以用 errors.As 检查特定类型
var se *specialError
if errors.As(e, &se) {
fmt.Printf(" 检测到 specialError: %s\n", se.msg)
}
}
}
fmt.Println()
// 使用 fmt.Errorf 包装多个错误
wrapped := fmt.Errorf("外层错误: (第一个: %w), (第二个: %w), (第三个: %w)", errA, errB, errC)
// 实际的错误链结构取决于 %w 的顺序和格式化字符串
// 例如,这里更像是 errA 被 wrapped 包裹,errB 被包裹 errA 的错误包裹,以此类推(具体取决于实现)
// 或者,它们可能被视为并列地被一个包含描述文字的错误所包裹。
// 为了清晰,我们假设一种简单的线性包裹(虽然内部实现可能更复杂,但 errors.Unwrap 行为类似)
fmt.Println("使用 fmt.Errorf 解包 (逐层):")
currentErr := wrapped
i := 1
for currentErr != nil {
fmt.Printf(" 解包层级 %d: %v (类型: %T)\n", i, currentErr, currentErr)
var se *specialError
if errors.As(currentErr, &se) { // 检查当前错误或其链中的错误
fmt.Printf(" 在链中检测到 specialError: %s\n", se.msg)
}
// errors.Is 也可以用于检查链中的特定错误实例
if errors.Is(currentErr, errA) {
fmt.Println(" 在链中检测到 错误A")
}
unwrapped := errors.Unwrap(currentErr)
if unwrapped == currentErr || i > 5 { // 防止无限循环或过多层级
break
}
currentErr = unwrapped
i++
}
}
运行该示例,我们将得到预期的输出:
使用 errors.Join 解包:
原始错误 1: 错误A (类型: *errors.errorString)
原始错误 2: 特殊错误B (类型: *main.specialError)
检测到 specialError: 特殊错误B
原始错误 3: 错误C (类型: *errors.errorString)
使用 fmt.Errorf 解包 (逐层):
解包层级 1: 外层错误: (第一个: 错误A), (第二个: 特殊错误B), (第三个: 错误C) (类型: *fmt.wrapErrors)
在链中检测到 specialError: 特殊错误B
在链中检测到 错误A
结合上述两个示例,我们可以看到:
Go 1.20 同时提供这两种能力,让开发者在处理多个错误时有了更灵活的选择。理解它们的细微差别,有助于我们根据具体场景做出最合适的决策。
Go 1.20 引入的 errors.Join() 无疑为 Go 语言的错误处理工具箱增添了一件强大的新工具。它在特定场景下——如输入验证、并行任务错误收集、defer 中的多错误处理——能够显著提升代码的表达力和用户体验,使得我们能够向调用者或用户提供更全面、更友好的错误信息。
然而,正如社区的讨论所揭示的,它并非“银弹”,更不应被滥用以取代“快速失败”这一久经考验的错误处理黄金法则。理解 errors.Join() 的适用边界,审慎评估其在具体场景下的收益与成本(如可能带来的信息过载或对 API 错误契约的破坏),是每一位 Gopher 都需要具备的判断力。
最终,优雅的错误处理,在于清晰、明确、以及在“最小惊奇”与“详尽信息”之间找到那个恰到好处的平衡点。errors.Join() 为我们实现这种平衡提供了一种新的可能性。
社区讨论帖:https://www.reddit.com/r/golang/comments/1ldyywj/use_errorsjoin/
聊一聊,也帮个忙:
欢迎在评论区留下你的经验、思考和问题。如果你觉得这篇文章对你有帮助,也请转发给你身边的 Gopher 朋友们,让更多人参与到关于 Go 错误处理的深度讨论中来!
精进有道,更上层楼
极客时间《Go语言进阶课》上架刚好一个月,受到了各位读者的热烈欢迎和反馈。在这里感谢大家的支持。目前我们已经完成了课程模块一『语法强化篇』的 13 讲,为你系统突破 Go 语言的语法认知瓶颈,打下坚实基础。
现在,我们已经进入模块二『设计先行篇』,这不仅包括 API 设计,更涵盖了项目布局、包设计、并发设计、接口设计、错误处理设计等构建高质量 Go 代码的关键要素。
这门进阶课程,是我多年 Go 实战经验和深度思考的结晶,旨在帮助你突破瓶颈,从“会用 Go”迈向“精通 Go”,真正驾驭 Go 语言,编写出更优雅、更高效、更可靠的生产级代码!
扫描下方二维码,立即开启你的 Go 语言进阶之旅!
商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。
© 2025, bigwhite. 版权所有.
2025-06-19 08:16:53
本文永久链接 – https://tonybai.com/2025/06/19/language-design-in-the-era-of-llm
大家好,我是Tony Bai。
大型语言模型 (LLM) 的浪潮正以前所未有的速度和深度席卷软件开发领域。从代码生成、Bug 修复到文档撰写,AI 似乎正成为每一位开发者身边无所不能的“副驾驶”。在这股浪潮中,一个略显“刺耳”但又无法回避的论调开始浮现,正如一篇引人深思的博文《Programming Language Design in the Era of LLMs: A Return to Mediocrity?》中所指出的那样:“一切都更容易用 Python 实现 (Everything is Easier in Python)”——当然,这里指的是在 LLM 的强力辅助下。
这并非危言耸听。文章中展示的图表(来源于论文 “Knowledge Transfer from High-Resource to Low-Resource Programming Languages for Code LLMs“)清晰地揭示了一个趋势:LLM 在那些训练数据量巨大的“高资源”语言(如 Python, JavaScript, Java, C# 等)上,代码生成和任务解决的效能显著高于像 Go、Rust 这样的“低资源”语言:
如果 LLM 能够如此轻松地用 Python(或其他高资源语言)根据自然语言需求生成大部分“胶水代码”甚至核心逻辑,那么我们不禁要问:
今天,我们就来聊聊在 LLM 时代,DSL 和像 Go 这样的通用语言,其未来的价值究竟何在。
领域特定语言 (DSL) 的核心价值在于“专为特定领域而生”。通过精心设计的语法和语义,DSL 能够:
文章中那个视频游戏对话的例子就非常典型:从繁琐的 API 调用序列
# example code for a VN
character.draw("alice", character.LEFT, 0.1)
character.draw("bob", character.RIGHT, 0.1)
character.say("alice", "hello there!")
character.say("bob", "hi!")
character.state("alice", "sad")
character.say("alice", "did you hear the news?")
到简洁的 DSL 描述
# example DSL for dialog
[ alice @ left in 0.1, bob @right in 0.1 ]
alice: hello there!
bob: hi!
alice[sad]: did you hear the news?...
DSL 的优势一目了然。
然而,LLM 的出现,似乎正在侵蚀 DSL 的这些传统护城河。当开发者可以用自然语言向 Copilot 或 ChatGPT 描述“我想要一个能让 Alice 和 Bob 在屏幕两侧对话的场景”,并且 LLM 能够直接生成 Python 或 JavaScript 代码来实现这个功能时,我们不禁要问:为什么还要费心去学习、设计、构建和推广一个全新的 DSL 呢?
这里隐含的“机会成本”的问题非常现实:
这引发了一个令人担忧的趋势:DSL 的发展是否会因此停滞不前?语言设计的多样性是否会因此受到冲击,最终导致“人人皆写 Python (在 LLM 辅助下)”的局面?
Go语言虽然在全球拥有数百万开发者,并且在云原生、后端开发等领域占据主导地位,但在 LLM 的训练数据占比上,相较于 Python、JavaScript 等拥有更长历史和更广泛应用场景(尤其是 Web 前端、数据科学等产生大量开源代码的领域)的语言,仍然处于“低资源”状态。
这意味着,LLM 在直接生成高质量、复杂 Go 代码方面的能力,目前可能还无法与它在 Python 等语言上的表现相媲美。 这对 Go 社区和开发者来说,既是挑战,也是反思和寻求新机遇的契机。
挑战:
然而,Go 语言的独特优势在 LLM 时代或许会更加凸显:
面对大型语言模型(LLM)带来的挑战,编程语言的设计(无论是领域特定语言(DSL)还是通用语言如 Go)并非只能被动应对。学术界正在探索一些富有前景的新方向,旨在实现语言设计与 LLM 的协同进化,而非零和博弈。
首先,有研究提出教会 LLM 理解 DSL 的方法,核心思路是利用 LLM 擅长的语言(如 Python 的受限子集)来表达核心逻辑。由于 LLM 对特定 DSL 的理解和生成能力有限,开发者可以设计工具或方法,将这些 Python 表达式“提升”或自动翻译到目标 DSL 中。这一思路启示未来的 DSL 设计者应考虑为其语言提供一个 LLM 友好的“语义映射层”,例如用 Python 或其他高资源语言来描述其核心概念和操作。
其次,在 DSL 中弥合“形式化”与“非形式化”的鸿沟也是一个重要方向。开发者在编写复杂系统内核时,往往需要精确控制每一行代码,此时 LLM 的帮助有限。然而,在编写不常用的“一次性”脚本时,LLM 能够根据自然语言描述生成“胶水代码”,使得开发者只需关注核心的“有趣”部分。因此,未来的 DSL 设计可以探索如何无缝集成“非形式化”自然语言描述,作为规范、注释,甚至直接融入代码中。与此同时,是否可以从 DSL 的类型系统或静态分析结果中,自动生成高质量的自然语言规范,反过来帮助 LLM 更好地理解和生成 DSL 代码,值得深入研究。
最后,面向 LLM 辅助验证的语言设计也成为一种趋势。研究者们不再满足于 LLM 生成“能运行”的代码,而是期望 LLM 能生成带有形式化规约(specifications)的代码,并利用验证语言(如 Dafny、Boogie)来证明这些代码的正确性。这一趋势对 DSL 和通用语言(如 Go)的设计提出了新要求,开发者需要考虑如何更好地支持“规约即代码”和“验证即开发”的模式。例如,Go 语言的强类型和接口设计,为形式化验证提供了一定的基础,未来的改进可以在此基础上进一步发展。
通过以上几个方向的探索,编程语言设计有望与 LLM 实现更为紧密的协同进化,推动软件开发的进步和创新。
LLM 的崛起,无疑对整个编程语言生态带来了深刻的冲击和前所未有的挑战。那种“学会一门语言,用好一个框架,就能高枕无忧”的时代可能正在远去。
“一切皆可用 Python (在 LLM 辅助下)”的论调,虽然略显夸张,但也点出了一个事实:对于那些仅仅是为了减少样板代码、提供简单抽象的 DSL,或者在表达力和生态丰富度上不及 Python 的通用语言,其生存空间确实受到了挤压。
然而,这并不意味着语言设计本身会走向“平庸化”或消亡。相反,LLM 可能会迫使我们重新思考编程语言的核心价值:
最终,技术的浪潮会淘汰掉不适应变化的,也会催生出新的、更强大的生命体。对于我们开发者而言,保持学习的热情,理解不同工具的本质和边界,拥抱变化,或许才是应对这个“AI 定义一切”时代的不二法门。
你认为 LLM 会如何改变你使用的编程语言?Go 和 DSL 的未来将走向何方?欢迎在评论区留下你的真知灼见!
精进有道,更上层楼
极客时间《Go语言进阶课》上架刚好一个月,受到了各位读者的热烈欢迎和反馈。在这里感谢大家的支持。目前我们已经完成了课程模块一『语法强化篇』的 13 讲,为你系统突破 Go 语言的语法认知瓶颈,打下坚实基础。
现在,我们即将进入模块二『设计先行篇』,这不仅包括 API 设计,更涵盖了项目布局、包设计、并发设计、接口设计、错误处理设计等构建高质量 Go 代码的关键要素。
这门进阶课程,是我多年 Go 实战经验和深度思考的结晶,旨在帮助你突破瓶颈,从“会用 Go”迈向“精通 Go”,真正驾驭 Go 语言,编写出更优雅、
更高效、更可靠的生产级代码!
扫描下方二维码,立即开启你的 Go 语言进阶之旅!
感谢阅读!
如果这篇文章让你对AI时代的DSL和通用语言设计和未来有了新的认识,请帮忙转发,让更多朋友一起学习和进步!
商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。
© 2025, bigwhite. 版权所有.
2025-06-18 08:37:48
本文永久链接 – https://tonybai.com/2025/06/18/inside-goroutine-scheduler-column
你好,我是Tony Bai。
欢迎踏上一次深入Go并发核心的探索之旅——【Go并发调度艺术】微专栏。我们每天都在使用go关键字轻松驾驭并发,享受着Go语言带来的编程乐趣。但在这简洁的背后,是一套复杂而精密的调度系统在默默支撑。它如同一位技艺精湛的指挥家,巧妙地调度着成千上万的goroutine,在用户态的轻盈与操作系统的力量之间取得了绝妙的平衡。
许多Go开发者为了面试,会去“背诵”GMP模型的概念,记忆那些零散的知识点。但这种学习方式往往浮于表面,难以形成深刻的理解,更不用说将其内化为指导我们编写高效并发程序的工程直觉。
这一次,我们换个视角,不再是被动接受结论,而是主动参与“设计”。
本微专栏的核心特色,是跟随Go调度器的核心设计者之一Dmitry Vyukov的思考路径(基于其经典的Go调度器设计资料)。我们将设身处地,从他最初面临的设计目标和挑战开始,一步步看他是如何分析问题、尝试方案、做出权衡,并最终构建出我们今天所熟知的、强大的Go调度器的。
在这个微专栏中,你将“亲历”:
我们的目标是,通过这种“问题驱动”和“设计者视角”的学习方式:
三篇深度探索,为你揭示:
无论你是希望在技术深度上更进一步,还是想在面试中展现对Go并发的透彻理解,亦或是对计算机系统底层原理充满好奇,这个微专栏都将是一次不容错过的思想盛宴。
这不仅仅是一次对“Go原理”的学习,更是一次与顶尖工程师设计思想的碰撞与共鸣。
准备好成为Go调度器设计过程的“参与者”了吗?扫下方二维码订阅,让我们一起,拨开并发调度的层层迷雾,探寻其核心的艺术与智慧。
更多有关Go、AI以及云原生的深度微专栏,请点击【微专栏】合集选择订阅。
商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。
© 2025, bigwhite. 版权所有.