标签 泛型 下的文章

认知负荷对编程语言选择和学习的影响

本文永久链接 – https://tonybai.com/2024/10/24/cognitive-load-impact-on-programming-language-choice-and-study

在《Go语言精进之路:从新手到高手的编程思想、方法和技巧》两卷书出版后,我收到了一些读者的反馈。其中一位读者提到:“为什么作者如此偏爱使用心智负担这个词?”当时我对此并未给予太多关注。然而,近期我阅读了一些关于认知心理学和脑科学的著作后,才意识到读者的反馈不仅仅是对该词频繁使用的关注,更可能暗示了用词不当的问题。

“心智负担”(Mental Load)指的是在处理多任务或日常生活安排时所需耗费的心理资源和精力,包括记忆、计划、组织以及应对各种任务所带来的精神压力。然而,在学习、思考和理解的情境中,特别是在编程语言的学习中,使用“认知负荷”(Cognitive Load)这一术语可能更为恰当。

认知负荷理论最初由澳大利亚新南威尔士大学的认知心理学家约翰·斯威勒(John Sweller)于1988年首先提出来的,旨在解释学习过程中的认知资源分配。认知负荷是指在学习、思考或解决问题时,大脑在处理信息和执行任务时所承受的负担。在选择编程语言时,认知负荷是一个至关重要的因素,指的是人们在学习和使用某种编程语言时,为理解语法、掌握工具和解决问题所需付出的心理负担和精力。

那么,在面对众多主流编程语言时,在不考虑市场需求与公司或组织强制学习的情况下,认知负荷究竟如何影响开发人员对编程语言的选择呢?在这篇文章中,我将进行一些不那么严谨,也非专业的粗略探讨,希望能够为大家带来一些启发。

1. 认知负荷在编程语言中的体现

认知负荷理论发展到今天,其总体被分为三种类型:

  • 内在认知负荷(Intrinsic Cognitive Load)

内在认知负荷,也称为固有负荷,是由学习材料本身的复杂性所决定的,它与学习任务的本质和内容密切相关。例如,编程语言的语法规则、数据类型内存管理并发模型等都是内在负荷的一部分。学习这些概念的难易程度主要取决于编程语言本身的设计和复杂度。

  • 外在认知负荷(Extraneous Cognitive Load)

外在负荷是由学习环境和教学方式引起的负担,通常是由于无关信息或低效的学习方法造成的。比如,配置开发环境、学习非必要的工具或被复杂的IDE界面困扰,都可能增加外在负荷。在编程语言学习中,清晰的文档和易于理解的教程可以显著减少外在负荷。

虽然外在负荷不是由编程语言语法本身决定的,但它会影响新手的学习体验。如果学习资源和工具太复杂或不直观,即使是简单的编程语言也会让人感到困难。

  • 相关认知负荷(Germane Cognitive Load)

相关认知负荷是指学习过程中专门用于理解、整合和构建知识结构的认知努力。它与思维加工、模式识别、知识内化等过程有关。在编程中,相关认知负荷指的是学习者在掌握编程思想、设计模式和编程习惯时所付出的努力。例如,理解如何在实际项目中应用编程概念,如何优化代码设计,以及如何解决编程中的复杂问题,这些过程都会增加相关认知负荷。这种负担是积极的,因为它有助于深入理解和长期记忆。

下面这张图来自网络,可以帮助我们进一步理解三类认知负荷(只是出发点来自教学角度):

由此可见,对于新手来说,学习一门编程语言时,外在认知负荷是第一道门槛,它决定了是否能坚持学习,还是选择“Hello and Bye”;内在认知负荷则是基础,是核心;相关认知负荷则是进阶挑战,决定了可以达到的高度

接下来,我们将针对一些主流编程语言,沿着新手入门学习编程语言的认知负荷先后顺序进行粗略对比。希望这能为大家提供在编程语言选择方面的有用信息,同时帮助不同阶段的学习者针对各自的认知负荷水平做好心理准备。

2. 主流编程语言的认知负荷对比

在探讨主流编程语言的认知负荷时,我们需要从外在认知负荷、内在认知负荷以及相关认知负荷这三个维度进行深入分析。这种分析不仅能帮助我们理解不同语言的特点,更能为选择合适的编程语言提供参考依据。

注:笔者是后端程序员出身,对前端语言比如Javascript、Typescript等了解有限,因此这里将使用像Go、Rust、C++等主流后端语言作为分析和对比的参考对象。

2.1 外在认知负荷的影响

在编程语言学习的初始阶段,外在认知负荷往往是最先遇到的挑战

Python在这方面表现出色,它简单的环境搭建流程让初学者能够快速开始编程之旅。只需安装一个解释器,新手就能立即开始编写代码。虽然在使用pip管理依赖时可能遇到一些包冲突的问题,但整体来说,在环境搭建、工具使用等外在认知负荷方面对初学者相当友好。

Go语言同样提供了令人称道的开发体验。它的工具链安装过程直观明了,跨平台支持也十分完善。特别值得一提的是,自从Go 1.11引入go modules以来,依赖管理变得更加自动化和直观。虽然对新手来说,理解版本控制可能需要一些时间。此外,Go团队也给出了Go项目布局的官方建议,为开发者进行代码组织提供了清晰的参考。

相比之下,C++的环境搭建则显得较为复杂。开发者需要安装编译器,配置IDE,这些步骤对新手来说都构成了不小的挑战。加上缺乏统一的包管理工具(尽管vcpkgconan等工具正在改变这一现状),以及灵活但缺乏标准的项目结构,都让C++的外在认知负荷明显高于其他语言。

Rust通过其官方工具链安装工具rustup提供了相对简便的环境搭建方式。它的Cargo包管理器集成度高,使用便捷,而且项目结构的标准化程度高,这些特点都有效降低了外在认知负荷。

Java则介于两个极端之间。它需要安装JDK并配置环境变量(如JAVA_HOME、CLASS_PATH等),这个过程对新手来说可能有些繁琐。虽然Maven和Gradle这样的依赖管理工具功能强大,但学习曲线较陡峭。不过,Java严格的项目布局规范在初期可能显得死板,但从长远来看反而有助于培养良好的工程习惯。

过了环境安装、工具使用和项目布局这些“外在认知负荷”的关卡后,语言自身的复杂性便会成为新手面前的更大的挑战。

2.2 内在认知负荷考量

谈到语言本身的复杂性,Python的设计理念“简单胜于复杂”使其成为认知负荷最低的选择之一。它的语法接近自然语言,几乎不需要特别的学习就能读懂基本的代码结构。这种简洁性使得Python特别适合编程初学者,以至于主流的儿童编程教学大多使用Python(当然一些启蒙教学使用的是scratch)。

Go语言同样以简洁著称,它的语法设计注重一致性和可读性。虽然保留了指针这样的底层特性,可能会让某些初学者感到困惑,但整体而言,Go的学习曲线相当平缓。值得注意的是,Go 1.18引入泛型后,虽然提升了语言的表达能力,但也增加了一定的复杂性。至于Go是否适合作为从零开始编程的新手,也是见仁见智。

C++的内在认知负荷则明显较高。它支持多种编程范式,包括面向过程、面向对象、模板编程等,这些范式和特性固然强大,但对初学者来说往往构成了较大的认知负担。特别是在处理多态、模板元编程等高级特性时,学习曲线会变得异常陡峭。

Rust的内在认知负荷同样不低,但事实证明其复杂性是有意义的。它的所有权系统和借用检查器虽然增加了学习难度,但这些机制对于理解系统编程的本质非常有帮助,同时提高了程序在运行时的安全性。新手在最初接触这些概念时可能会感到困惑,但掌握后会对内存安全有深刻的理解。

Java的内在认知负荷介于中等水平。它的面向对象语法虽然比Python或Go略显繁琐,但整体而言还算直观。Java的复杂性主要体现在面向对象设计模式、泛型和异常处理等特性上,这些概念需要时间来消化和掌握。

2.3 相关认知负荷的深入分析

在实际应用知识解决问题时,各种语言呈现出不同的特点。

Python的优势在于它能让学习者快速将知识付诸实践。其丰富的标准库和生态、简洁的语法使得从学习到应用的过程异常顺畅。无论是数据科学还是Web开发,Python都能让新手快速看到成果。它支持多种编程范式,并且社区的PEP 8规范为代码风格提供了清晰的指导。

Go语言在知识应用方面同样表现出色。它的工具链完善,容易将所学付诸实践。特别是在服务器端开发领域,Go的并发模型和简洁的语法让新手能够相对轻松地构建高效的后端服务。虽然Go不像传统的面向对象语言那样依赖继承体系,但其接口机制和组合方式为代码设计提供了优雅的解决方案。

C++的相关认知负荷较高,主要体现在将理论知识转化为实践时面临的挑战。内存管理和性能优化这些概念需要大量实践才能真正掌握。它支持多种编程范式,这种灵活性虽然强大,但对初学者来说往往是一把双刃剑。由于缺乏统一的编码规范,新手可能在选择最佳实践时感到困惑。

Rust在这方面呈现出独特的特点。它的所有权系统要求开发者在实践中深入思考内存管理问题,这个过程虽然充满挑战,但却能培养扎实的系统编程思维。Rust社区提供的编码规范和工具链都很完善,有助于形成良好的编程习惯。

Java则以其企业级开发的特点著称。它要求开发者深入理解面向对象编程的核心概念,这个过程需要较长时间的积累。Java的设计模式体系完备,社区的编码规范成熟,这些特点有助于培养专业的工程思维,但对新手来说可能需要更多的时间和耐心。

2.4 综合评估

通过以上分析,我们可以看出不同语言在认知负荷方面的特点。

Python以其全方位的低认知负荷成为初学者的理想选择。

Go语言通过简洁的设计和完善的工具链在降低认知负荷方面做出了显著成效。

Java虽然相对繁琐,但其成熟的生态系统和规范的开发流程为长期发展提供了良好基础。

Rust和C++的学习曲线较陡,但它们在系统编程和性能优化方面的深度让投入的学习成本变得有价值。

在理解了编程语言的认知负荷特点后,我们不妨再从心理学的角度,特别是借助三脑理论的视角,来探讨初学者是如何在面对不同编程语言时做出选择的。

3. 初学者的编程语言学习决策过程

三脑理论(Triune Brain Theory)由Paul D. MacLean于1970年提出的理论假说,该理论将人脑分为三个层次,如下图所示:


来自维基百科

  • 爬虫脑(Reptilian Brain):也称原始脑,负责基本生存反应,包括对威胁的快速反应和本能行为。
  • 情绪脑(Limbic System):处理情绪和动机,影响记忆形成和社交行为。
  • 理性脑(Neocortex):负责高级认知功能,如逻辑思考、语言处理和复杂决策。

注:三脑理论提出较早,如今有新的理论认为三脑理论毫无依据。不过这里我们假定这个理论是正确和适用的。

三脑理论影响初学者的编程学习决策的过程是怎样的呢?这个过程往往涉及本能反应(爬虫脑主导)、情感体验(情绪脑主导)和理性思考(理性脑主导)三个层面的互动。我们继续往下看。

3.1 初学阶段的决策历程

在首次接触编程语言时,学习者的反应往往是多层次的。本能层面的反应最为直接,面对像C++这样认知负荷较高的语言时,很多人会本能地产生畏惧感。这种反应不是简单的怯懦,而是大脑对复杂性的自然防御机制。相反,Python这类认知负荷较低的语言则较少触发这种应激反应,使得学习者能够保持相对轻松的心态。

情感层面的体验则更为复杂。当成功运行第一个程序时,无论使用什么语言,都会带来成就感。但随着学习的深入,不同语言带来的情感体验会产生分化。举个例子,我在早期学习Java时,仅仅是配置环境变量这样的基础工作就带来了挫折感,这种负面情绪很容易影响学习的积极性。而Rust虽然入门门槛较低,但一旦进入到所有权系统的学习,很多人会因为频繁的编译错误而感到沮丧。

理性思考则是决策过程中最后但也是最重要的环节。这包括对语言应用领域的评估、职业发展前景的考虑,以及个人学习时间和精力投入的权衡。这个阶段的决策通常更加慎重,也更具有长期性。

3.2 深入学习阶段的转变

随着学习的深入,最初的决策依据往往会发生改变。原本令人望而生畏的特性可能转变为吸引力的来源。这种转变在Rust的学习过程中特别明显,当开发者逐渐理解了所有权系统的价值,最初的困惑可能转化为对语言设计的欣赏

在这个阶段,情感体验也往往变得更加丰富。克服困难带来的成就感可能超越了简单的编程快感,这也解释了为什么一些看似“难学”的语言反而能够培养出更加忠实的用户群体。Rust连续多年在最受欢迎编程语言榜单上位居前列,很大程度上就源于这种深层的技术认同感

理性思考在这个阶段会更加全面,不再局限于语言本身的特性,而是扩展到整个技术生态系统的考量。开发者会更多地思考语言的性能特点、社区活跃度、工具链完善程度等因素。

3.3 认知负荷与学习效果

从短期来看,低认知负荷的语言确实能够提供更平缓的学习曲线,让入门过程更加顺畅。Python和Go在这方面的优势明显,它们能让学习者快速进入实践阶段,建立信心。但这种便利性有时也会带来一个意想不到的问题:学习者可能在掌握了基础语法后陷入平台期,难以实现质的突破。这也是为什么经常有读者询问如何才能在Go语言编程中更进一步

相比之下,高认知负荷的语言虽然入门较难,但往往能够培养更深入的编程思维。比如Rust的所有权系统,虽然增加了学习难度,但这种设计迫使开发者深入思考内存管理的问题,从而建立更扎实的系统编程基础。C++的模板元编程虽然复杂,但掌握后能够大大提升代码的抽象能力和复用效率。

不过,我们也要警惕过高的认知负荷带来的风险。如果学习过程中的挫折感持续累积,很容易导致半途而废。每年入门一次Rust的真实案例也屡见不鲜。这就要求我们在选择编程语言时,既要考虑个人的学习能力和时间投入,也要权衡职业发展的需求,找到一个适合自己的平衡点。

4. 小结

在探讨了认知负荷对编程语言学习的影响后,我们可以得出一些粗浅的见解:编程语言的学习绝非简单的语法掌握过程,而是一个涉及多个认知维度的复杂历程。从开发环境的搭建到语言特性的理解,从基础概念的掌握到工程实践的应用,每个阶段都会给学习者带来不同程度的认知压力。理解这些认知负荷的本质,有助于我们做出更明智的编程语言学习的选择。

对于编程新手来说,像Python和Go这样在各个维度都尽量降低认知负荷的语言,无疑是入门的理想选择。但我们也要认识到,较高的认知负荷未必就是缺点。就像Rust和C++这样的语言,它们的学习曲线虽然陡峭,但这种”困难”往往蕴含着宝贵的学习机会。通过克服这些认知挑战,开发者能够建立起更深入的系统编程认知,形成更扎实的技术功底。

选择合适的编程语言,某种程度上就像选择一位长期相处的伙伴。这个选择不仅要考虑语言本身的特点,还要权衡个人的学习能力、职业规划和时间投入。认知负荷理论为我们提供了一个有价值的分析框架,但最终的选择还是要回归到个人的实际需求和发展目标。正如没有完美的编程语言一样,也没有放之四海而皆准的学习路径。找到适合自己的平衡点,或许才是最务实的学习策略。

最后,在人工智能编码辅助技术飞速发展的今天,开放的学习心态和持续学习的能力,可能比选择某个特定的编程语言更为重要。毕竟唯一不变的可能就是变化本身。

5. 参考资料


Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时,我们也会加强代码质量和最佳实践的分享,包括如何编写简洁、可读、可测试的Go代码。此外,我们还会加强星友之间的交流和互动。欢迎大家踊跃提问,分享心得,讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落,享受coding的快乐! 欢迎大家踊跃加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com

我的联系方式:

  • 微博(暂不可用):https://weibo.com/bigwhite20xx
  • 微博2:https://weibo.com/u/6484441286
  • 博客:tonybai.com
  • github: https://github.com/bigwhite
  • Gopher Daily归档 – https://github.com/bigwhite/gopherdaily
  • Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

Go语言演进的双保险:GOEXPERIMENT与GODEBUG

本文永久链接 – https://tonybai.com/2024/10/11/go-evolution-dual-insurance-goexperiment-godebug

Go语言自诞生以来就以其简洁、高效和强大的并发支持而闻名,Go团队承诺保持Go1向后兼容性,以确保用户的代码在未来的版本中继续正常运行。然而,保持语言的稳定性与不断创新(增加新特性)之间的平衡一直是Go团队面临的挑战。为了应对这一挑战,Go语言引入了两个关键机制:GOEXPERIMENT和GODEBUG来平衡新功能的试验、稳定发布和向后兼容。这两个机制共同构成了Go语言特性发布的“双保险”,确保语言能够稳步前进的同时,不会因为激进的改变而影响现有代码的稳定性。本文就来简单探讨一下这两个机制是如何保障Go语言新特性稳定发布的。

1. GOEXPERIMENT:新特性的摇篮

GOEXPERIMENT是一个Go语言的环境变量,是用于控制实验性特性的机制。它允许开发者在编译时(使用go build、go install、go run或go test)启用一些尚未正式发布的语言特性或优化。通过GOEXPERIMENT,Go团队能够在正式发布之前广泛测试新功能,收集反馈并进行必要的调整。

比如,在今年8月发布的Go 1.23版本发布了一个实验特性:带有类型参数的type alias,就像下面代码一样,我们可以在编译时开启该实验特性:

// github.com/bigwhite/experiments/blob/master/go1.23-examples/lang/generic_type_alias.go

$GOEXPERIMENT=aliastypeparams go build generic_type_alias.go
$./generic_type_alias
Int Slice: [1 2 3 4 5]
String Slice: [hello world]
Person Slice: [{Alice 30} {Bob 25}]

如果不开启实验特性,上述的代码就会编译失败:

// github.com/bigwhite/experiments/blob/master/go1.23-examples/lang/generic_type_alias.go

$go build generic_type_alias.go
# command-line-arguments
./generic_type_alias.go:5:6: generic type alias requires GOEXPERIMENT=aliastypeparams

我们看到:通过设置GOEXPERIMENT=featureflag可以开启对应的实验特性,如果要同时开启多个实验特性,可以用逗号分隔的实验特性列表,就像下面这样:

$GOEXPERIMENT=featureflag1,featureflag2,...,featureflagN go build

那么如何查看当前Go版本有哪些实验验特性可用呢?我们可以借助go doc工具,以go 1.23.0为例:

$go doc goexperiment.Flags
package goexperiment // import "internal/goexperiment"

type Flags struct {
    FieldTrack        bool
    PreemptibleLoops  bool
    StaticLockRanking bool
    BoringCrypto      bool

    // RegabiWrappers enables ABI wrappers for calling between
    // ABI0 and ABIInternal functions. Without this, the ABIs are
    // assumed to be identical so cross-ABI calls are direct.
    RegabiWrappers bool
    // RegabiArgs enables register arguments/results in all
    // compiled Go functions.
    //
    // Requires wrappers (to do ABI translation), and reflect (so
    // reflection calls use registers).
    RegabiArgs bool

    // HeapMinimum512KiB reduces the minimum heap size to 512 KiB.
    //
    // This was originally reduced as part of PacerRedesign, but
    // has been broken out to its own experiment that is disabled
    // by default.
    HeapMinimum512KiB bool

    // CoverageRedesign enables the new compiler-based code coverage
    // tooling.
    CoverageRedesign bool

    // Arenas causes the "arena" standard library package to be visible
    // to the outside world.
    Arenas bool

    // CgoCheck2 enables an expensive cgo rule checker.
    // When this experiment is enabled, cgo rule checks occur regardless
    // of the GODEBUG=cgocheck setting provided at runtime.
    CgoCheck2 bool

    // LoopVar changes loop semantics so that each iteration gets its own
    // copy of the iteration variable.
    LoopVar bool

    // CacheProg adds support to cmd/go to use a child process to implement
    // the build cache; see https://github.com/golang/go/issues/59719.
    CacheProg bool

    // NewInliner enables a new+improved version of the function
    // inlining phase within the Go compiler.
    NewInliner bool

    // RangeFunc enables range over func.
    RangeFunc bool

    // AliasTypeParams enables type parameters for alias types.
    // Requires that gotypesalias=1 is set with GODEBUG.
    // This flag will be removed with Go 1.24.
    AliasTypeParams bool
}
    Flags is the set of experiments that can be enabled or disabled in the
    current toolchain.

    When specified in the GOEXPERIMENT environment variable or as build tags,
    experiments use the strings.ToLower of their field name.

    For the baseline experimental configuration, see objabi.experimentBaseline.

    If you change this struct definition, run "go generate".

go doc输出结果中的Flags结构体其实是$GOROOT/internal/goexperiment包中的一个类型,这个类型每一个字段对应一个实验特性,字段名的小写即可作为GOEXPERIMENT的值,比如AliasTypeParams的小写形式aliastypeparams正是我们在前面示例中使用的实验特性。

在Flags结构体中,我们看到了几个十分熟悉的字段,比如LoopVar、RangeFunc、Arenas等,这些实验特性有些已经正式落地,比如:Go 1.21引入的实验特性LoopvarGo 1.22版本中成为正式语法特性。而Arenas这个在Go 1.20版本引入的实验特性则因为实现上缺陷而迟迟不能转正,目前处于proposal hold状态

Go对实验特性的引入分为两种情况:

  • 默认开启实验特性,无需在编译时通过GOEXPERIMENT=featureflag显式开启

在Go 1.22中的exectracer2就是这样一个实验特性,它控制着是否使用新的execution trace的实现。

对于这样的实验特性,我们可以通过GOEXPERIMENT=nofeatureflag对其进行显式关闭,以Go 1.22引入的实验特性ExecTracer2为例,可以使用下面命令关闭该实验特性:

$GOEXPERIMENT=noexectracer2 go build

注:之后使用go version your-go-app,可以看到“your-go-app: go1.22.0 X:noexectracer2”的输出。

  • 默认不开启实验特性,需在编译时通过GOEXPERIMENT=featureflag显式开启

这就是我们最熟悉的实验特性引入方式,Go 1.23的AliasTypeParams实验特性就是默认不开启的,前面的例子已经给出了开发方法,这里就不赘述了。

实验特性通常经过1到2个版本的实验便会落地,成为正式特性。已经落地的实验特性通常会从Flags结构体中移除,比如Go 1.22的goexperiment.Flags结构体中的ExecTracer2,在Go 1.23中就看不到了。但总有一些已经落地的实验特性对应的flag字段依然还留存在Flags结构体里,比如:LoopVar,这个原因还不得而知!并且这样的已经成为正式特性的Flag,我们也无法再通过GOEXPERIMENT=nofeatureflag对其进行显式关闭了,因为它已经不再是实验特性了!

不过有些实验特性即便转正落地了,也会考虑到新特性对legacy code行为的影响而去读取go.mod中的go version再决定是否应用新特性,比如LoopVar。LoopVar转正后,该特性也仅在编译的包来自于包含声明Go 1.22或更高版本的模块时适用,比如:Go 1.22或Go 1.23。这可以确保没有程序会因为简单地采用新的Go版本而改变行为,我们来看一个例子:

// go.mod

module demo

go 1.20

// main.go
package main

import (
    "fmt"
    "time"
)

func main() {
    var m = [...]int{1, 2, 3, 4, 5}

    for i, v := range m {
        go func() {
            time.Sleep(time.Second * 3)
            fmt.Println(i, v)
        }()
    }

    time.Sleep(time.Second * 5)
}

我们使用go 1.23.0版本编译该包,并运行输出的程序:

$go build
$./demo
4 5
4 5
4 5
4 5
4 5

可以看到,即便使用了Go 1.23版本,但因当前module的go version依然是go 1.20,Go编译器默认不会开启loopvar特性。

不过如果我们显式使用GOEXPERIMENT=loopvar,go编译器便不会考虑go.mod文件中的go version是什么版本,都会开启loopvar新特性:

$GOEXPERIMENT=loopvar go build
$./demo
4 5
1 2
0 1
2 3
3 4

Go编译器会有一套Go试验特性的默认值,如果你通过GOEXPERIMENT显式开启了某些特性,导致该特性flag值与默认值不同,那么我们可以通过go version命令查看到这些不同之处。以上面GOEXPERIMENT=loopvar go build构建出的demo为例:

$go version demo
demo: go1.23.0 X:loopvar

目前Go官方尚没有一个专门的页面用于汇总GOEXPERIMENT的各个flag的随Go版本release的历史,我们只能通过Flag字段在go issues查找其对应的issue来重温当时的情况。

到这里,我们可以看到GOEXPERIMENT引入的实验特性机制可以让Go团队相对稳健的向Go语言引入新特性(虽然不是所有新特性都需要走式样特性的流程,比如对泛型的支持等),但是当新特性破坏了向后兼容,或者Go团队要对现有特性的错误语义(比如panicnil)进行变更时,Go1这个严格的兼容性规则就很可能成为阻碍在大家面前的一道门槛!为了在保持兼容性和推动创新之间取得平衡,Go团队就需要一种新的机制,通过渐进式的方法来引入破坏性(break change)的变更,这就是GODEBUG控制机制,下面我们就来说说GODEBUG。

2. GODEBUG:在运行时控制特性行为的开关

GODEBUG也是一个Go环境变量,和GOEXPERIMENT用于构建时不同,GODEBUG用在运行时控制Go程序的某些行为。它允许开发者临时将某一特性恢复到旧的行为,即使在新版本中该特性的默认行为已经发生了改变。

GODEBUG的设置形式为逗号分隔的key=value对,例如:

$GODEBUG=http2client=0,http2server=0 ./your-go-app

这个设置会禁用客户端和服务器端对HTTP/2的使用。

上面是使用GODEBUG禁用新特性的例子。对于存量特性语义或实现变更,比如Go 1.23版本对time.Timer和Ticker进行了重实现,新实现底层使用了无缓冲channel,但通过下面设置可以恢复原先实现中的带缓冲channel:

$GODEBUG=asynctimerchan=1 ./your-go-app

考虑到兼容性而进行的GODEBUG设置将在至少两年(四个Go版本)内保持。但一些设置,例如http2client和http2server,将会更长时间地保持,甚至是无限期的。

除了GODEBUG环境变量之外,Go还提供了其他几种进行特性行为设置的方式,下面我们来看看。

3. GODEBUG、go:debug和go.mod中godebug directive的关系

3.1. //go:debug指令

从Go 1.21开始,可以在源代码中使用//go:debug指令来设置GODEBUG的值。这些指令必须放在文件的顶部,在package语句之前。例如:

//go:debug panicnil=1
//go:debug asynctimerchan=0
package main

这些指令会在编译时被处理,并影响生成的二进制文件的行为。

3.2 go.mod中的godebug指令

从Go 1.23开始,可以在go.mod文件中使用godebug指令来设置GODEBUG的默认值,例如:

// go.mod

godebug (
    default=go1.21
    panicnil=1
    asynctimerchan=0
)

这个配置会影响整个模块(module)的默认GODEBUG设置。

3.3 优先级和应用范围

那么GODEBUG、//go:debug以及go.mod中的godebug指令的优先级关系是怎样的呢?

显然,环境变量GODEBUG优先级最高,因为它可以在运行时覆盖其他设置,适用于临时调试或特定运行环境。

go:debug指令优先级次之,通常应用于特定的main包,适用于对特定程序进行精细控制。

而go.mod中的godebug指令优先级最低,为整个模块设置默认值,适用于项目级别的配置。

基于上述关系,我们来看看一个Go应用GODEBUG设置的默认值的确定过程。当没有显示设置GODEBUG环境变量时,各设置的默认值按以下顺序确定:

  • 首先查看用于构建程序的Go工具链(版本)的默认值。
  • 然后根据go.mod或go.work中声明的Go版本(go version)进行调整。
  • 之后应用go.mod中的godebug指令(如果有的话)。
  • 最后是//go:debug,通常仅应用于main module。

例如,如果一个项目的go.mod声明了go 1.20,那么即使使用Go 1.21工具链编译,也会默认使用panicnil=1(即允许panic(nil))。

不过有特殊情况需要注意,比如对于声明早于Go 1.20版本的项目,GODEBUG默认值会被配置为匹配Go 1.20的行为,而不是更早的版本;又比如在测试环境中,*_test.go文件中的//go:debug指令会被视为测试主包的指令等。

这么看规则还是蛮复杂的,那么编译后待执行的程序的默认GODEBUG的设置究竟是什么呢?我们可以通过go version -m来查看,以gopls v0.16.2为例:

$go version -m /Users/tonybai/Go/bin/gopls
/Users/tonybai/Go/bin/gopls: go1.23.0
    path    golang.org/x/tools/gopls
    mod golang.org/x/tools/gopls    v0.16.2 h1:K1z03MlikHfaMTtG01cUeL5FAOTJnITuNe0TWOcg8tM=
    dep github.com/BurntSushi/toml  v1.2.1  h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
    dep github.com/google/go-cmp    v0.6.0  h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
    dep golang.org/x/exp/typeparams v0.0.0-20221212164502-fae10dda9338  h1:2O2DON6y3XMJiQRAS1UWU+54aec2uopH3x7MAiqGW6Y=
    dep golang.org/x/mod    v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
    dep golang.org/x/sync   v0.8.0  h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
    dep golang.org/x/telemetry  v0.0.0-20240829154258-f29ab539cc98  h1:Wm3cG5X6sZ0RSVRc/H1/sciC4AT6HAKgLCSH2lbpR/c=
    dep golang.org/x/text   v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
    dep golang.org/x/tools  v0.22.1-0.20240829175637-39126e24d653   h1:6bJEg2w2kUHWlfdJaESYsmNfI1LKAZQi6zCa7LUn7eI=
    dep golang.org/x/vuln   v1.0.4  h1:SP0mPeg2PmGCu03V+61EcQiOjmpri2XijexKdzv8Z1I=
    dep honnef.co/go/tools  v0.4.7  h1:9MDAWxMoSnB6QoSqiVr7P5mtkT9pOc1kSxchzPCnqJs=
    dep mvdan.cc/gofumpt    v0.6.0  h1:G3QvahNDmpD+Aek/bNOLrFR2XC6ZAdo62dZu65gmwGo=
    dep mvdan.cc/xurls/v2   v2.5.0  h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8=
    build   -buildmode=exe
    build   -compiler=gc
    build   DefaultGODEBUG=asynctimerchan=1,gotypesalias=0,httplaxcontentlength=1,httpmuxgo121=1,httpservecontentkeepheaders=1,panicnil=1,tls10server=1,tls3des=1,tlskyber=0,tlsrsakex=1,tlsunsafeekm=1,winreadlinkvolume=0,winsymlink=0,x509keypairleaf=0,x509negativeserial=1
    build   CGO_ENABLED=1
    build   CGO_CFLAGS=
    build   CGO_CPPFLAGS=
    build   CGO_CXXFLAGS=
    build   CGO_LDFLAGS=
    build   GOARCH=amd64
    build   GOOS=darwin
    build   GOAMD64=v1

我们看到其DefaultGODEBUG如下:

DefaultGODEBUG=asynctimerchan=1,gotypesalias=0,httplaxcontentlength=1,httpmuxgo121=1,httpservecontentkeepheaders=1,panicnil=1,tls10server=1,tls3des=1,tlskyber=0,tlsrsakex=1,tlsunsafeekm=1,winreadlinkvolume=0,winsymlink=0,x509keypairleaf=0,x509negativeserial=1

相对于GOEXPERIMENT的flags的数量,GODEBUG的设置项更多,下面我们根据go官方资料整理一个GODEBUG设置项列表供大家参考(信息截至2024.10.7)。

4. GODEBUG设置的历史演进

下表按照Go版本顺序列出了各个GODEBUG设置,包括它们被引入的版本、含义以及如何开启和关闭它们:

不过请注意以下几点:

  • 默认值可能会随着Go版本的更新而改变。
  • 某些设置可能在未来的Go版本中被移除。
  • 部分设置(如tlsmaxrsasize)允许指定具体的数值,而不仅仅是0或1。
  • 有些设置(如multipartmaxheaders和multipartmaxparts)在默认情况下是无限制的,需要明确设置一个数值来启用限制。

5. 小结

在Go语言的演进过程中,GOEXPERIMENT和GODEBUG两个机制起到了至关重要的作用。GOEXPERIMENT为新特性的实验和测试提供了灵活的环境,使得开发者可以在正式发布之前尝试和反馈新功能,从而确保Go语言的创新不会影响到已有代码的稳定性。通过这种方式,Go团队能够逐步引入新特性,同时维持向后兼容性。

另一方面,GODEBUG则为开发者提供了在运行时控制特性行为的工具,使得新版本引入的破坏性更改能够被临时禁用。这种灵活性使得开发者有一个平滑过渡的机会,能够在更新的同时,保证应用的平稳运行,避免了因语言更新而导致的潜在问题,使Go能够在保持稳定性的同时不断创新。

总的来说,这两个机制共同构成了Go语言特性发布的“双保险”,确保了语言的持续发展与稳定性之间的平衡。这一策略不仅促进了Go语言的创新,也增强了开发者的信心,使其能够在不断变化的环境中有效地编写和维护代码。

6. 参考资料


Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时,我们也会加强代码质量和最佳实践的分享,包括如何编写简洁、可读、可测试的Go代码。此外,我们还会加强星友之间的交流和互动。欢迎大家踊跃提问,分享心得,讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落,享受coding的快乐! 欢迎大家踊跃加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com

我的联系方式:

  • 微博(暂不可用):https://weibo.com/bigwhite20xx
  • 微博2:https://weibo.com/u/6484441286
  • 博客:tonybai.com
  • github: https://github.com/bigwhite
  • Gopher Daily归档 – https://github.com/bigwhite/gopherdaily
  • Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言进阶课 AI原生开发工作流实战 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