Go 1.25新提案:GOMAXPROCS默认值将迎Cgroup感知能力,终结容器性能噩梦?

本文永久链接 – https://tonybai.com/2025/04/09/gomaxprocs-defaults-add-cgroup-aware

Go官方出手!新提案自动优化容器内GOMAXPROCS,告别性能噩梦!

在Kubernetes等容器环境中运行Go应用时,一个常见的性能陷阱悄然存在:默认的GOMAXPROCS值基于节点CPU核心数,而非Pod的CPU限制(limit),导致资源争抢和性能下降。近期一篇广受关注的博客文章“Golang Performance Penalty in Kubernetes”通过实测数据揭示了这一问题带来的显著延迟增加(高达65%+)和吞吐量降低(近20%)。

不过近期,Go核心团队带来一则好消息,Go Runtime团队的Michael Pratt已正式提出一项提案(#73193),旨在让Go运行时默认感知Linux Cgroup的CPU quota限制并自动调整GOMAXPROCS值,该提案有望在Go 1.25中为开发者带来开箱即用的性能优化,告别在容器或Kubernetes中手动配置GOMAXPROCS的烦恼。

在这篇文章中,我会对当前GOMAXPROCS默认值在云原生环境引发的性能问题以及Pratt的提案做一个详细说明,供广大Gopher们参考。

1. 容器中GOMAXPROCS的“水土不服”与性能代价

Go 1.5版本起,GOMAXPROCS默认设置为“可用的CPU核心数”(综合考虑机器核心数和CPU亲和性设置)。这在单租户或资源不受严格限制的环境下工作良好。然而,在普遍使用Cgroup进行资源隔离的容器化部署场景中,这一默认行为却常常与Pod的实际CPU限制limits.cpu)产生严重错位,引发一系列性能问题。

想象一下:一个Go应用部署在拥有32个vCPU的K8s节点上,但其Pod的limits.cpu被设置为1。Go运行时看到的是32核,于是默认将GOMAXPROCS设为32。这意味着Go运行时会尝试并发运行多达32个操作系统线程来执行Go代码,而Kubernetes(通过Cgroup的CPU Quota机制)却严格限制该Pod在每个调度周期内(如100ms)只能使用相当于1个CPU的计算时间。

这会带来什么后果? 正如Mansoor Majeed在其博客文章《Golang Performance Penalty in Kubernetes》中通过基准测试所生动展示的:

  • 过度的上下文切换

32个活跃的Go线程争抢远少于此的可用CPU时间片(在此例中仅相当于1个CPU的时间),迫使操作系统内核进行大量、且低效的线程上下文切换。在他的测试中,错误配置GOMAXPROCS的场景下,上下文切换次数(context_switches_total)相比正确配置时飙升了近4倍(从约6.5k/s 增加到30k/s)。

  • CPU配额扼杀(Throttling)与调度延迟

应用(尤其CPU密集型任务,如博客中的Fibonacci计算)的并发线程迅速耗尽Cgroup分配的CPU时间配额(cpu.cfs_quota_us)。一旦耗尽,内核将强制暂停该Cgroup内所有线程的执行,直到下一个调度周期(cpu.cfs_period_us)开始。这直接导致了请求处理的延迟尖峰。博客中的”Process Schedule Stats”图表也显示,错误配置下,进程等待CPU的时间(Waiting for CPU)出现了高达34秒的峰值,而正确配置下仅约900毫秒。

  • 应用性能显著下降

过度的上下文切换和频繁的CPU Throttling共同作用,导致应用端到端的性能大幅降低。博客的wrk基准测试显示,在CPU密集场景下,与正确设置GOMAXPROCS=1相比,使用默认GOMAXPROCS=32(基于节点而非Pod限制)导致的性能下降如下图所示:

我们看到:平均请求延迟增加了65% (从 20ms 上升到 33ms),最大请求延迟增加了82% (从255ms飙升到465ms)。整体RPS (每秒请求数) 下降了近20% (从50213减少到40356)。

  • GC 放大问题

Go的并发垃圾回收器(GC)的工作量与GOMAXPROCS挂钩。GC目标是使用25%的P(对应GOMAXPROCS数量)进行后台标记工作,并在空闲的P上运行额外的 idle worker。过高的GOMAXPROCS会导致GC期间产生远超实际可用CPU资源的并发请求,极易触发或加剧CPU配额扼杀,即使在非GC期间应用本身运行平稳。极端情况下,由于内核调度,可能出现大量GC worker同时运行,短暂“冻结”用户goroutine的执行。

  • 运行时扩展性成本

运行更高的GOMAXPROCS会带来额外的运行时开销,例如每个P的本地缓存(如mcache)导致的内存占用增加,以及P之间进行工作窃取、GC协调等所需的同步成本。当GOMAXPROCS远大于实际可用CPU时,这些成本被白白支付,却无法带来相应的并行处理收益。

容器中GOMAXPROCS默认设置为节点CPU数量这个问题在Go社区存在已久,相关讨论见于#33803。目前,开发者通常采用以下方式规避:

  • 手动设置环境变量

比如:在Kubernetes Deployment YAML中,通过valueFrom: resourceFieldRef将GOMAXPROCS环境变量显式设置为Pod的limits.cpu值,下面是一个示例:

spec:
  containers:
  - name: my-go-app
    image: my-go-app:latest
    env:
    - name: GOMAXPROCS
      valueFrom:
        resourceFieldRef:
          # Ensure the resource name matches your limit spec
          resource: limits.cpu
          # Use divisor 1 for whole cores, or adjust if using millicores
          # and need integer conversion logic (though GOMAXPROCS needs integer)
          # Often, just referencing limits.cpu works if it's a whole number.
          # For fractional limits resulting in non-integer GOMAXPROCS,
          # manual calculation or automaxprocs might be better.
          divisor: "1"
    resources:
      limits:
        cpu: "2" # Example limit
      requests:
        cpu: "100m"
  • 使用第三方库

在Go代码中引入如uber-go/automaxprocs这样的库,它会在应用启动时自动检测Cgroup v1或v2的CPU限制,并相应地调用runtime.GOMAXPROCS()进行设置。

import _ "go.uber.org/automaxprocs"

func main() {
    // automaxprocs automatically adjusts GOMAXPROCS during init
    // ... rest of your application
}

虽然有解决方案,但这需要开发者意识到问题的存在并主动采取措施,增加了配置负担和潜在的疏漏风险。近期Go官方终于有针对此问题的动作了,我们来详细看看官方的方案。

2. 官方提案:让GOMAXPROCS自动适配CPU Limit

为了一劳永逸地解决这个问题,并提供更优的开箱即用体验,Go核心团队成员pratt在#73193中提出了一个具体的解决方案,旨在将Cgroup CPU limit感知能力内置到Go运行时中。下面也简单说一下Pratt给出的方案的核心机制,包括以下几点:

  • 自动检测CPU Limit

在程序启动时,如果用户未通过环境变量GOMAXPROCS指定值,Go运行时(仅在Linux 上)将主动检测以下三项:

(a) 机器的总CPU核心数: 通过runtime.NumCPU()的底层机制获取。
(b) CPU亲和性限制: 通过sched_getaffinity(2) 系统调用获取当前进程允许运行的CPU核心集合大小。
(c) Cgroup CPU Quota限制: 运行时会查找进程所属的Cgroup层级结构(支持v1和v2,以及混合模式)。对于每一层级,它会读取cpu.cfs_quota_us 和cpu.cfs_period_us(v1) 或cpu.max(v2) 文件。计算出每一层的CPU limit(等效核心数=quota/period)。最终取整个层级路径上的最小值作为该进程的“有效CPU limit”。

  • 计算新的默认GOMAXPROCS

新的默认GOMAXPROCS值将是上述(a)、(b)、(c)三者计算结果中的最小值。特别地,由(c)计算出的Cgroup limit值在用于最终比较前会经过一个调整:adjusted_cgroup_limit = max(2, ceil(effective_cpu_limit))。即,先向上取整,然后确保结果至少为2。

  • 自动更新

为了适应CPU限制或亲和性可能在运行时发生变化的情况(例如 Kubernetes的 “in place vertical scaling” 特性允许动态调整Pod的limits.cpu),Go运行时将引入一个后台机制(可能在sysmon协程中实现),以较低频率(例如,提案建议最小周期30秒,最长1分钟)定期重新检查CPU亲和性设置和Cgroup的CPU quota文件。如果检测到变化导致计算出的默认GOMAXPROCS值改变,运行时将自动调用内部的GOMAXPROCS设置函数进行更新。

  • 引入新的API

该提案还引入了一个新的公共API:runtime.SetDefaultGOMAXPROCS()。调用此函数会立即触发一次上述默认值的计算和设置过程,忽略GOMAXPROCS 环境变量的影响。这可以用于覆盖启动时通过环境变量设置的值,恢复到运行时自动检测的行为。同时,在得知外部环境(如Cgroup 配置)发生变化后,主动强制进行一次更新,而不必等待后台的自动扫描。

  • 兼容性控制

这是一个可能改变现有程序行为的变更。为了提供平滑的过渡和控制能力,该新行为将由一个GODEBUG标志cgroupgomaxprocs=1控制。根据Go的GODEBUG兼容性策略,对于go.mod文件中指定的Go语言版本低于引入该特性的版本(预计是Go 1.25),该标志默认为0 (禁用新行为,保持现状)。只有当项目将其go.mod中的Go版本升级到1.25或更高时,默认值才会变为1 (启用新行为)。开发者仍然可以通过设置GODEBUG=cgroupgomaxprocs=0 来显式禁用新行为。

3. 其他设计考量与细节

经过#33803几年的讨论,Pratt在新提案中也谈及了一些设计考量和细节,这里也就一点典型的问题做一下梳理:

  • 为何是Limit而非Shares/Request?

Cgroup的cpu.shares(v1)或cpu.weights(v2)(对应Kubernetes的CPU Request)定义的是资源竞争时的相对优先级,而不是硬性的CPU使用上限。当系统负载不高时,仅设置了Request 的容器可能使用远超其Request值的CPU。因此,Shares/Weights不适合作为限制并行度的GOMAXPROCS的依据。Java和.NET在其运行时中进行容器资源感知的实践也得出了类似的结论,它们都选择基于CPU Quota(Limit)。

  • 处理分数Limit(Rounding)

Cgroup Quota可以设置成分数形式(如limits.cpu:”1500m”对应1.5核)。由于GOMAXPROCS必须是整数,提案选择向上取整 (ceil)。例如,1.5会变成2。这样做的考虑是,允许应用利用Cgroup提供的突发能力,并且可能更好地向监控系统指示CPU饥饿状态。然而,这与uber-go/automaxprocs默认向下取整 (floor) 的策略不同。后者认为分数部分的配额可能是为容器内的辅助进程(如sidecar、监控agent)或C库线程预留的,向下取整更保守,避免Go进程完全用尽配额。这是一个开放的讨论点,最终实现可能会根据社区反馈调整。

  • 最小值为2的理由

提案建议将通过Cgroup limit计算出的值(向上取整后)与2比较,取较大者。即,即使CPU limit小于1(如0.5),最终也会至少设置为2。这样做的主要原因是GOMAXPROCS=1会完全禁用Go调度器的并行性,可能导致一些意想不到的性能问题或行为怪异,例如GC worker可能在运行时暂时“暂停”用户Goroutine(因为只有一个P可以运行,需要在用户代码和GC代码间切换)。设置至少为2可以保留基本的并行能力,更好地利用Cgroup允许的突发性。当然,如果物理核心数或CPU亲和性限制本身就是1,那么根据前面的计算规则,最终GOMAXPROCS仍然会是1。

  • 日志

与automaxprocs提供可选的日志输出不同,该提案的内置实现默认不打印关于GOMAXPROCS被自动调整的日志信息,以保持运行时输出的简洁性。

4. 小结

这项针对Go运行时的提案(#73193) 若能在Go 1.25实现,将为容器化环境中的Go应用带来实质性改进。其核心优势在于开箱即用的性能优化:通过自动将GOMAXPROCS与Cgroup CPU Limit对齐,避免了因配置不当导致的常见性能瓶颈(如高延迟、低吞吐)。这将极大简化开发者的运维工作,无需再手动设置GOMAXPROCS或依赖automaxprocs等第三方库。同时,其自动更新机制也使应用能更好地适应K8s等平台的动态资源调整。

当然,该提案并非万能。它主要解决了设置了CPU Limit的场景。对于仅设置CPU Request(旨在利用空闲资源)的Pod,此变更目前不会带来直接改善,GOMAXPROCS仍将基于节点或亲和性设置。如何优化这类场景下的资源利用率,仍是未来值得探索的方向。

总而言之,#73193提案是Go社区直面云原生环境中一个长期痛点的关键举措。它有望将更智能、更自动化的资源感知能力内置到运行时,显著提升Go应用在容器中的默认性能表现和易用性。我们期待该提案的最终落地,并建议开发者关注其后续进展。

你是否也在K8s中遇到过GOMAXPROCS的困扰?欢迎在评论区分享你的经验和看法!

5. 参考资料


Gopher部落知识星球在2025年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。并且,2025年将在星球首发“Gopher的AI原生应用开发第一课”、“Go陷阱与缺陷”和“Go原理课”专栏!此外,我们还会加强星友之间的交流和互动。欢迎大家踊跃提问,分享心得,讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落,享受coding的快乐! 欢迎大家踊跃加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格6$/月。有使用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 testing包将迎来新增强:标准化属性与持久化构件API即将落地

本文永久链接 – https://tonybai.com/2025/04/07/go-testing-add-attr-and-artifactdir

Go语言的testing包即将迎来两项备受期待的增强功能:标准化的测试属性(Test Attributes)和测试构件(Test Artifacts)管理。这两项提案(#43936 和#71287)均已获得Go团队的批准或高度认可,旨在显著提升Go测试的可观测性、调试效率以及与外部工具链(如CI/CD系统、测试管理平台)的集成能力。本文将深入解读这两项提案的设计理念、核心API、应用场景及其对Go开发者的潜在影响。

1. Go测试过程中的“痛点”

长期以来,Go开发者在处理测试过程中的元数据和输出文件时,常常面临一些挑战,不得不依赖非标准的约定或变通方法,这直接影响了测试效率和工具集成的流畅性。

1.1 痛点一:脆弱且混乱的测试元数据传递

现代开发流程中,我们常常需要将测试与外部系统关联起来。例如,将自动化测试结果上报给TestRailAllure这样的测试管理平台,或者在CI/CD报告中直接链接到相关的Jira问题、代码提交或详细日志。

在t.Attr提案(#43936)出现之前,开发者通常只能通过t.Log或t.Logf输出特定格式的字符串来实现这一目标,例如类似以下的日志行:

// 示例:试图通过日志传递元数据
TESTRAIL_CASE_ID: C12345
JIRA_ISSUE: PROJ-789

这种方法的弊端显而易见:

  • 极其脆弱: 任何对日志格式、前缀或分隔符的微小改动,都可能导致依赖这些日志的外部解析工具(如CI脚本、报告生成器)失效。
  • 缺乏标准: 每个项目或团队可能会发明自己的格式,导致工具难以复用和维护。
  • 信息混杂: 重要的元数据与普通的测试日志信息混合在一起,增加了提取难度和误判的可能性。
  • 工具集成困难: 像go test -json这样的官方工具,其输出的Action: output 事件并不区分普通日志和这种“伪装”的元数据,下游消费者需要进行额外的、不可靠的字符串解析。

总之,这种方式给需要自动化处理测试结果的场景带来了持续的维护负担和不确定性。

当然痛点不限于此,我们再来看一个。

1.2 痛点二:转瞬即逝的测试构件,调试与归档的障碍

Go testing包提供了t.TempDir函数,用于创建测试期间使用的临时目录和文件,这在隔离测试状态方面非常有用。然而,t.TempDir的核心特性——在测试(无论成功或失败)结束后自动清理其内容——在某些场景下反而成了阻碍。想象以下常见情况:

  • 调试失败

一个复杂的集成测试失败了。测试过程中可能生成了详细的调试日志、服务间通信的网络抓包、或者是对比失败的实际输出文件。当你想检查这些文件以定位问题时,却发现它们随着测试的结束一同消失了。开发者不得不采取临时措施,比如注释掉t.Cleanup调用,或者在测试失败路径上手动复制文件到其他位置,过程繁琐且容易遗漏。

  • CI结果归档

在CI/CD流水线中,我们通常希望在测试失败时自动收集相关的诊断信息(如core dump、截图、性能剖析文件等)作为“构件(artifact)”进行归档,以便后续分析。虽然Go提供了-cpuprofile, -memprofile等标志并将结果放入-outputdir指定的目录,但对于测试代码自身产生的其他类型构件,缺乏一个统一且可靠的机制来指示它们需要被保留。

为了解决上述这些长期存在的痛点,Go社区积极讨论并推进了t.Attr和t.ArtifactDir这两项关键提案,旨在通过标准化的API为go testing包带来现代化的测试信息管理能力。

下面我们就来正式看看这两个提案究竟给我们带来了哪些测试过程中的便利。先来看看t.Attr提案。

2. t.Attr:为测试附加结构化元数据(#43936)

状态:已接受 (Accepted)

提案#43936旨在提供一种标准化的方式,将结构化的键值对元数据与特定的测试(或子测试)关联起来,并使其在go test -json的输出中易于访问。

2.1 核心API

该提案在testing.TB接口中增加了Attr方法,其定义如下:

package testing

type TB interface {
    // ... 其他方法

    // Attr 发出与此测试关联的测试属性。
    //
    // key不能包含空白字符。
    // 不同属性键的含义由持续集成系统和测试框架决定。
    //
    // 测试属性会立即在测试日志中发出,但应被视为无序的。
    Attr(key, value string)
}

开发者可以在测试代码中调用t.Attr(“myKey”, “myValue”)来记录元数据。经过社区的深入讨论,API最终确定为接受string类型的键和值。这主要是字符串简洁,易于理解和使用;与现有的主流测试管理系统(如 JUnit XML、Google 内部的 Sponge 系统)对属性/特性的定义(通常是string-string)保持一致。同时,还避免testing包引入对encoding/json的依赖。如果需要传递复杂结构,开发者可以自行将值JSON编码为字符串。

2.2 输出格式

t.Attr的调用会在标准测试日志中产生如下格式的输出:

=== ATTR  TestName <key> <value>

当使用go test -json运行时,test2json工具会将其转换为结构化的JSON事件:

{"Time": "...", "Action": "attr", "Package": "package/path", "Test": "TestName", "Key": "key", "Value": "value"}

go testing包增加了Attr后,在测试管理中,集成Go测试与系统如TestRail和Allure变得更加轻松,通过t.Attr可传递测试用例ID、特性标签和故事标签等信息。此外,测试输出中可以嵌入指向外部资源的链接,如日志系统、问题跟踪器(如Jira)、构建产物和文档。这种方式增强了CI/CD流程,使CI系统能够解析这些属性,以便于测试结果的分类、过滤和报告生成,或触发特定工作流,例如通过t.Attr(“environment”, “staging”)标记测试运行环境或关联代码提交哈希。最终,这种标准化的方法告别了脆弱的日志解析,提供了一种可靠的方式来提取测试元数据,取代了过去依赖特定日志前缀或格式的做法。

接下来,我们再来看看另外一个增强项:t.ArtifactDir。

3. t.ArtifactDir:持久化测试构件(#71287)

状态:很可能接受(Likely Accept)

提案#71287针对的是测试过程中产生的、可能需要后续检查的文件(即“测试构件(Artifact)”),它提供了一种机制,让开发者可以选择性地保留这些文件,而不是让它们被t.TempDir这种“阅后即焚”的特性自动删除。

3.1 核心API与标志

该提案在testing.TB接口中增加了ArtifactDir方法,其定义如下:

package testing

type TB interface {
    // ... 其他方法

    // ArtifactDir 返回一个目录供测试存储输出文件。
    // 当提供了 -artifacts 标志时,此目录将位于输出目录下。
    // 否则,ArtifactDir 返回一个临时目录,该目录在测试完成后被移除。
    //
    // 每个测试或子测试(在每个测试包内)都有一个唯一的构件目录。
    // 在同一测试或子测试中重复调用 ArtifactDir 返回相同的目录。
    // 子测试的输出不位于父测试的输出目录下。
    ArtifactDir() string
}

与此API配套的是一个新的go test命令行标志:-artifacts。它的行为特点如下:

  • 默认行为 (未指定-artifacts)

在这种情况下,t.ArtifactDir()的行为类似于t.TempDir(),返回一个临时目录,测试结束后其内容会被清理。这确保了测试行为的一致性,无论是否需要持久化构件。

  • 启用持久化 (指定-artifacts)

t.ArtifactDir()将返回一个位于-outputdir(默认为当前工作目录)下的特定目录,该目录及其内容在测试结束后不会被删除。

3.2 目录结构与输出

为了确保唯一性,尤其是在运行多个包(例如使用“./…”)或使用-count=N时,构件目录的路径结构经过了仔细考虑。最终采用的结构类似:

<outputdir>/<package_path>/<test_name>/<random_or_counter>

具体的路径转换和命名规则会进行必要的处理(如路径安全化、截断长名称等),但核心目标是提供一个可预测且唯一的存储位置。

当启用构件存储且测试首次调用ArtifactDir() 时,会输出类似信息:

=== ARTIFACTS TestName/subtest_name /path/to/actual/artifact/dir

在go test -json模式下,对应事件为:

{"Time":"...", "Action":"artifacts", "Package":"package/path", "Test":"TestName/subtest_name", "Path":"/path/to/actual/artifact/dir"}

其中Path字段包含了实际的构件目录路径。

综上,有了t.ArtifactDir()后,在调试失败的测试时,用户可以轻松检查测试生成的实际输出文件、对比文件、日志、核心 dump、网络抓包和性能剖析数据,而无需修改测试代码以阻止临时目录清理。此外,CI系统可以通过设置-artifacts和-outputdir标志,自动收集所有测试产生的构件,并将其存档或用于后续分析。在测试代码生成时,生成的代码可以输出到t.ArtifactDir()返回的目录,方便在验证失败时与预期的黄金文件进行对比。这种方法提供了一种官方推荐的方式来处理测试产物,减少了各个项目自行实现此类机制的需求。

4. 协同效应:属性与构件的强强联合

t.Attr和t.ArtifactDir这两个提案并非孤立存在,它们可以协同工作,提供更强大的测试信息管理能力。

最典型的场景是:使用t.ArtifactDir管理构件文件的存储,并使用t.Attr记录指向这些构件的元数据。

例如,一个测试可能会:

  • 调用dir := t.ArtifactDir()获取构件目录。
  • 在该目录中生成一个重要的日志文件,假设名为trace.log。
  • 调用t.Attr(“trace_log_path”, filepath.Join(dir, “trace.log”))来记录这个日志文件的确切路径。
  • 或者,如果CI系统会将构件上传到对象存储,测试可以记录其访问URL:t.Attr(“trace_log_url”, “s3://bucket/…”)。

这样,外部工具不仅知道测试产生了构件(通过Action: artifacts事件),还能通过解析Action: attr事件找到访问或描述这些构件的具体信息,实现了端到端的关联。

5. 小结

t.Attr和t.ArtifactDir的引入,标志着Go标准测试库在满足现代软件开发流程需求方面迈出了重要一步。它们通过提供标准化的API和工具链支持,极大地增强了测试的透明度、可调试性以及与自动化系统的集成深度。

随着这两个提案的落地(预计在未来的Go版本中),我们期待看到Go社区能够更轻松地构建健壮、可观测的测试体系,并与各种先进的开发运维工具无缝集成。这无疑将进一步巩固Go在构建可靠、高效软件系统方面的优势。开发者应密切关注这些新特性,并考虑如何在自己的项目中利用它们来改进测试实践。

6. 参考资料


Gopher部落知识星球在2025年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。并且,2025年将在星球首发“Gopher的AI原生应用开发第一课”、“Go陷阱与缺陷”和“Go原理课”专栏!此外,我们还会加强星友之间的交流和互动。欢迎大家踊跃提问,分享心得,讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落,享受coding的快乐! 欢迎大家踊跃加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格6$/月。有使用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语言精进之路1 Go语言精进之路2 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

文章

评论

  • 正在加载...

分类

标签

归档



Statcounter View My Stats