一句话概括,golang是一种高性能网络编程语言,仅此而已。
零值陷阱
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
type AllBasicTypes struct {
B bool
Str string
I8 int8
I16 int16
I32 int32
I64 int64
I int
U8 uint8
U16 uint16
U32 uint32
U64 uint64
U uint
Up uintptr
By byte // uint8
Ru rune // int32
F32 float32
F64 float64
C64 complex64
C128 complex128
}
// var v AllBasicTypes —— 各字段为零值:false、""、0、0.0、(0+0i) 等
golang为了规避Java的空指针引用而引入了默认零值的设计。这导致了在设计程序的时候,遇到零值要小心处理。
比如上述这个结构,变量声明 var v AllBasicTypes 之后访问 v.B会得到false,v.U8 会得到 0。
那么在实际的应用场景,就无法判断B到底是false,还是未赋值。解决的办法是另外加一个字典标记,或者使用 *bool 布尔指针这种奇怪的类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import (
"fmt"
"time"
"google.golang.org/protobuf/types/known/timestamppb"
)
func main() {
// 错误示例:使用 time.Time 的零值
var zeroTime time.Time // 或者 time.Time{}
protoTs := timestamppb.New(zeroTime)
// 序列化时就会报错
data, err := proto.Marshal(protoTs)
fmt.Println(err) // 报错:proto: Google.Protobuf.Timestamp.Seconds out of range -62135596800
}
而时间的零值导致的proto Timestamp溢出是隐形的错误。
我们可能在底层的模型代码里面使用 time.Time 定义了某个字段,但在顶层API获取的时候发现反序列化失败。
弱网问题
在弱网环境,golang几乎是一种非常难用的编程语言。内置的语言包非常薄弱,经常需要go get,而国内 go get 则必定绕不过网络代理。

即便网络问题解决了,第三方的菱形依赖你也解决了。但在弱网情况下,golang还是很难用。一份源代码,下载到本地,开发+编译绕不过 vscode + golang插件 + go mod tidy。
而且golang的vscode插件不是开箱即用的,安装之后还需要再安装 dlv 等工具。
兼容性问题
兼容性问题分两部分:操作系统兼容和语言向前兼容。
golang的不向前兼容
很多人不知道,其实golang在这么多年迭代的过程中出现过不向前兼容的变更:
| Go 版本 | 变更类别 | 具体不兼容变更描述 | 控制方式 / 备注 |
|---|---|---|---|
| Go 1 (2012) | 语言 & 标准库重大重构 | pre-Go 1 (r60 等) 到 Go 1 的巨大变更:包路径调整(e.g. encoding/asn1)、os.Error → error、time 包重构、map/delete 语法、rune 类型引入、map 遍历随机化等。 | 使用 go fix 工具辅助迁移。这是历史上最大的一次 breaking update。 |
| Go 1.1 | 语言 & 平台 | 整数除常量零变为编译错误;64 位平台 int/uint 变为 64 位;部分 net/syscall 结构体/签名变更。 | 直接影响编译或运行行为。 |
| Go 1.5 | 运行时 | GOMAXPROCS 默认从 1 改为 CPU 核心数(调度行为变更)。 | 性能/并发相关,可能影响旧假设。 |
| Go 1.21 | 运行时 & panic | panic(nil) 或 panic(untyped nil) 现在 panic *runtime.PanicNilError(之前无 panic)。 |
GODEBUG=panicnil=1 恢复旧行为(或 go 1.20 及更早)。 |
| Go 1.21 | 包初始化 | 包初始化顺序算法精确定义(按 import path 排序),之前为未定义行为。 | 依赖隐式初始化顺序的代码可能受影响。 |
| Go 1.22 | 语言(for 循环) | for 循环变量每个迭代创建新变量(之前所有迭代复用同一变量)。闭包捕获行为改变。 | 由 go.mod 中的 go 1.22(或更高)启用。旧模块保留旧行为。这是常见迁移点。 |
| Go 1.22 | net/http.ServeMux | ServeMux 支持方法前缀(如 POST /path)、通配符 {name} 等新模式;路径转义处理变更。 |
GODEBUG=httpmuxgo121=1 恢复 Go 1.21 行为。 |
| Go 1.22 | go/types | 类型别名现在用 Alias 类型表示(之前等价于原类型)。 |
GODEBUG=gotypesalias=0(默认在 1.22);1.23 起默认 1,将在 1.27 移除。 |
| Go 1.22 | TLS & crypto | 默认最小 TLS 版本提升至 1.2;移除部分 RSA key exchange 和 3DES 等 cipher(后续版本)。 | 多个 GODEBUG(如 tls10server=1、tlsrsakex=1、tls3des=1),部分将在 1.27 移除。 |
| Go 1.23 | time 包 | time 包创建的 channel 变为 unbuffered(同步),影响 Timer.Stop 等正确使用。 |
GODEBUG=asynctimerchan=0 恢复旧异步行为(将在 1.27 移除)。 |
| Go 1.23 | net/http | http.ServeContent 在错误响应时移除某些 header。 |
GODEBUG=httpservecontentkeepheaders=1 恢复旧行为。 |
| Go 1.23 | x509 & TLS | 拒绝负序列号证书;Leaf 字段填充变更等。 |
多个 GODEBUG(如 x509negativeserial=0、x509keypairleaf=0)。 |
| Go 1.24 | x509 | x509 证书策略(Policies)字段使用变更。 | GODEBUG=x509usepolicies=0 恢复旧行为。 |
| Go 1.25 | 运行时 & nil 检查 | 某些 nil 指针 dereference(e.g. f, err := os.Open(); f.Name() 当 f==nil 时)现在严格立即 panic(之前某些情况延迟)。 |
严格遵守规范;无 GODEBUG 控制,需修复代码(先检查 err)。 |
| Go 1.25+ | 平台支持 | 移除对旧 OS 支持(如 macOS 11、32-bit windows/arm);Wasm 导出指令变更。 | 平台/移植相关,非 API 但影响构建。 |
在高版本golang中解析旧版本golang生成的证书,因为 x509部分的变更,可能出现解析失败的问题。
而语言不兼容的另一方面体现,是工具集的割裂。这在开发proto相关的时候就特别明显。
1
2
3
4
5
6
7
8
9
10
11
12
13
# 核心基础
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
# 数据验证
go install github.com/envoyproxy/protoc-gen-validate@latest
# HTTP 网关
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@latest
# API 文档
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@latest
go install github.com/google/gnostic/cmd/protoc-gen-openapi@latest
你安装了这么多的工具集。但当你切换到低版本的golang语言,就会发现 dlv 甚至都运行不了。
你需要找各种兼容当前版本的golang,最后在一堆工具集中迷失自我。
实际情况并不允许你一直使用最新版本的golang,所以最后行为链路会变得非常奇怪:
在window使用path路径切换到低版本golang –> 调试工具集不支持 –> 重新寻找支持当前版本的golang工具集ABCD –> A弄好了,弄B,B弄好了找C,然后一直循环往复。
最后你忘了其实你只是想要跑个程序,却一直把时间浪费在 go install xx version。
window不适合golang开发
而在window环境做golang开发是一个痛苦的事情。很多第三方工具,golang环境甚至无法编译成功,go install 就是一个笑话。
最后为了方便,golang开发大家都会尽可能地使用Linux/mac 系统。
选择编译缺失
Go 没有 C# / C 系那种预处理器级的条件编译:同一套语法里用 #if 把整段代码从未选中的构建里直接抹掉。Go 里更接近的做法是 build tags(//go:build)配合不同文件,或 runtime.GOOS 等运行时分支,语义和手感都不一样。若你从 C# 过来,会明显感到这种「选择性编译」能力的缺失。
C# 里常见用法大致如下(仅示意):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 1) 按内置/项目符号裁剪代码块(DEBUG 由 Debug 配置自动定义)
#if DEBUG
System.Diagnostics.Debug.WriteLine("只在 Debug 构建中存在");
#endif
#if MY_FEATURE
// 需在 csproj 的 <DefineConstants> 或编译参数里定义 MY_FEATURE
DoExperimentalStuff();
#endif
// 2) Conditional:未定义符号时,调用点会被编译器剔除(方法本身仍可存在)
using System.Diagnostics;
class App
{
[Conditional("VERBOSE")]
static void VerboseLog(string msg) => Console.WriteLine(msg);
static void Main()
{
VerboseLog("这条在未定义 VERBOSE 时不会产生调用指令");
}
}
对比之下,Go 要在「不同构建」下换实现,通常靠文件级条件(//go:build linux)或链接时注入,而不是在同一函数体里用 #if 折叠半页代码。
如果不用这种标签去做选择编译,那除了使用环境变量切换之外,我暂时想不到别的解决方案。
后记
写到这里,我突然想问自己一个问题:AI时代分享技术还有意义吗?
回想刚入行的时候,网络同行前辈的建议是写个博客记录整理一下遇到的问题和解决方案。至今这个习惯已经断断续续坚持了10年。
现在是AI时代,人学习信息技术的速度已经完全跟不上AI。但批判性思维让我不太受AI的影响,我始终把AI当成辅助计算的手段。
毕竟最终的决策风险要我本人承担。这么多年的风风雨雨,告诉我只有把命运掌握在自己手中,才不会被外物牵着走。
所以我觉得,写技术博客本身没有意义。思考本身就是意义,归纳是一个过程,文章只是一个供后人怀念的结果。
In one sentence: golang is a high-performance language for network programming—nothing more, nothing less.
Zero-value traps
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
type AllBasicTypes struct {
B bool
Str string
I8 int8
I16 int16
I32 int32
I64 int64
I int
U8 uint8
U16 uint16
U32 uint32
U64 uint64
U uint
Up uintptr
By byte // uint8
Ru rune // int32
F32 float32
F64 float64
C64 complex64
C128 complex128
}
// var v AllBasicTypes — zero values: false, "", 0, 0.0, (0+0i), etc.
To avoid null-pointer-style issues like in Java, golang uses default zero values. That means you must be careful with zero values when you design programs.
For the struct above, after var v AllBasicTypes, v.B is false and v.U8 is 0.
In real scenarios you cannot tell whether B is false or simply unset. The usual fixes are an extra map for “was it set?” or the odd choice of a *bool pointer.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import (
"fmt"
"time"
"google.golang.org/protobuf/types/known/timestamppb"
)
func main() {
// Bad example: zero value of time.Time
var zeroTime time.Time // or time.Time{}
protoTs := timestamppb.New(zeroTime)
// Serialization fails
data, err := proto.Marshal(protoTs)
fmt.Println(err) // e.g. proto: Google.Protobuf.Timestamp.Seconds out of range -62135596800
}
A zero time.Time quietly blows up proto Timestamp handling.
You might define a field as time.Time deep in the model, then fail deserialization at the top-level API.
Weak networks
On a poor network, golang is awkward to use: the standard library is thin, you constantly need go get, and in China go get almost always means fighting proxies.

Even if the network and third-party diamond dependencies are “solved,” golang is still painful under weak connectivity. You check out one repo and still cannot escape vscode + the golang extension + go mod tidy for dev and build.
The vscode extension is not turnkey either—you install it, then still install dlv and more.
Compatibility
Compatibility splits into OS-level compatibility and forward language compatibility.
When golang is not forward-compatible
Many people do not realize golang has shipped backward-incompatible changes over the years:
| Go version | Category | What broke | Mitigation / notes |
|---|---|---|---|
| Go 1 (2012) | Language & stdlib overhaul | Huge jump from pre-Go 1 (r60, etc.): package paths (e.g. encoding/asn1), os.Error → error, time redesign, map/delete, rune, randomized map iteration, … |
Use go fix. Largest breaking migration ever. |
| Go 1.1 | Language & platforms | Integer divide-by-zero on constants is an error; int/uint 64-bit on 64-bit; some net/syscall shapes/signatures. |
Direct compile/runtime impact. |
| Go 1.5 | Runtime | GOMAXPROCS default 1 → number of CPUs. |
Concurrency/perf assumptions may change. |
| Go 1.21 | Runtime & panic | panic(nil) / untyped nil panics *runtime.PanicNilError (previously no panic). |
GODEBUG=panicnil=1 or stay on go 1.20 or older. |
| Go 1.21 | Package init | Init order is now defined (import path order); previously undefined. | Code relying on implicit init order may break. |
| Go 1.22 | Language (for) |
Per-iteration loop variables (was one shared var). Closure capture changes. | Enabled by go 1.22 (or newer) in go.mod; older modules keep old behavior. Common migration point. |
| Go 1.22 | net/http.ServeMux |
Method prefixes (POST /path), {name} wildcards, escaping changes. |
GODEBUG=httpmuxgo121=1 restores Go 1.21 behavior. |
| Go 1.22 | go/types |
Type aliases use Alias (previously same as underlying type). |
GODEBUG=gotypesalias=0 (default in 1.22); default 1 from 1.23; removed 1.27. |
| Go 1.22 | TLS & crypto | Min TLS 1.2; some RSA KEX / 3DES ciphers removed (later releases). | Various GODEBUG flags (tls10server=1, …), some removed by 1.27. |
| Go 1.23 | time |
Channels from time are unbuffered; affects correct Timer.Stop usage. |
GODEBUG=asynctimerchan=0 (removed 1.27). |
| Go 1.23 | net/http |
http.ServeContent strips some headers on errors. |
GODEBUG=httpservecontentkeepheaders=1. |
| Go 1.23 | x509 & TLS |
Reject negative serials; Leaf population changes, etc. |
GODEBUG such as x509negativeserial=0, x509keypairleaf=0. |
| Go 1.24 | x509 |
Certificate Policies field handling. | GODEBUG=x509usepolicies=0. |
| Go 1.25 | Runtime & nil checks | Some nil derefs (e.g. f, err := os.Open(); f.Name() when f==nil) panic immediately (was sometimes deferred). |
Spec-correct; no GODEBUG; fix code (check err first). |
| Go 1.25+ | Platforms | Drops old OS support (e.g. macOS 11, 32-bit windows/arm); Wasm export changes. | Porting/build impact, not always API. |
Parsing certs produced by older golang with a newer toolchain can fail because of x509 changes.
Another face of incompatibility is splintered tooling—especially obvious around proto.
1
2
3
4
5
6
7
8
9
10
11
12
13
# Core
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
# Validation
go install github.com/envoyproxy/protoc-gen-validate@latest
# HTTP gateway
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@latest
# API docs
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@latest
go install github.com/google/gnostic/cmd/protoc-gen-openapi@latest
You install all of that—then drop to an older golang and even dlv may not run.
You hunt for toolchains that match, and get lost in the matrix of plugins.
You cannot always stay on the newest golang, so workflows get weird:
Tweak path on window to an old golang → debugger stack unsupported → hunt for tools A B C D that match → fix A, then B, then C, loop forever.
You only wanted to run a program, but time burns on go install version pins.
window is a bad fit for golang
On window, many third-party tools fail to build; go install becomes a joke.
So people doing golang gravitate to Linux / mac when they can.
No selective compilation
Go lacks preprocessor-style conditional compilation like C# / the C family: #if that strips whole regions from unselected builds. The closest Go offers is build tags (//go:build) across files, or runtime.GOOS branches—different semantics and ergonomics. Coming from C#, you feel the missing “selective compilation.”
Typical C# patterns (illustrative):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 1) Trim code by symbol (DEBUG is defined in Debug builds)
#if DEBUG
System.Diagnostics.Debug.WriteLine("Only in Debug builds");
#endif
#if MY_FEATURE
// Define MY_FEATURE in csproj <DefineConstants> or compiler flags
DoExperimentalStuff();
#endif
// 2) Conditional: if the symbol is undefined, call sites are stripped (method may remain)
using System.Diagnostics;
class App
{
[Conditional("VERBOSE")]
static void VerboseLog(string msg) => Console.WriteLine(msg);
static void Main()
{
VerboseLog("No call instruction if VERBOSE is not defined");
}
}
By contrast, Go swaps implementations per build mostly via file-level conditions (//go:build linux) or link tricks—not #if folding half a function.
Without those tags, aside from environment variables, I do not have another clean pattern.
Postscript
Writing this, I ask myself: does sharing tech still matter in the AI era?
When I started, seniors said: blog your problems and fixes. I have kept that habit, off and on, for ten years.
We cannot out-learn AI—but critical thinking keeps me from being swept along; I treat AI as a calculator.
Risk is still mine. Years of bumps taught me: hold your fate in your own hands, or the world drags you.
So the blog post itself is not the point. Thinking is the point—synthesis is the process; the article is just something for later readers to remember you by.
ひと言で言えば、golangは高性能なネットワーク向け言語に過ぎない。
ゼロ値の罠
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
type AllBasicTypes struct {
B bool
Str string
I8 int8
I16 int16
I32 int32
I64 int64
I int
U8 uint8
U16 uint16
U32 uint32
U64 uint64
U uint
Up uintptr
By byte // uint8
Ru rune // int32
F32 float32
F64 float64
C64 complex64
C128 complex128
}
// var v AllBasicTypes — 各フィールドはゼロ値: false、""、0、0.0、(0+0i) など
Javaのようなヌルポインタ問題を避けるため、golangはデフォルトゼロ値を採用した。その結果、設計時にゼロ値には常に注意が必要になる。
上の構造体では var v AllBasicTypes のあと、v.Bはfalse、v.U8は0になる。
実務では、Bが本当にfalseなのか、未代入なのか判別できない。対策は別マップで「セットされたか」を持つか、変な型の *bool を使うことになる。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import (
"fmt"
"time"
"google.golang.org/protobuf/types/known/timestamppb"
)
func main() {
// 悪い例: time.Time のゼロ値を使う
var zeroTime time.Time // または time.Time{}
protoTs := timestamppb.New(zeroTime)
// シリアライズで失敗する
data, err := proto.Marshal(protoTs)
fmt.Println(err) // 例: proto: Google.Protobuf.Timestamp.Seconds out of range -62135596800
}
時間のゼロ値が原因のproto Timestampまわりの不具合は、見えにくい。
下位のモデルで time.Time フィールドを定義していても、上位のAPIでデシリアライズに失敗することがある。
弱いネットワーク
回線が弱い環境では、golangはかなり使いづらい。標準付属は薄く、すぐgo getが必要で、中国国内ではgo getはプロキシとセットになる。

ネットの問題とサードパーティのダイヤモンド依存をどうにかしても、弱い回線のもとではgolangは相変わらず厳しい。ソースを取得しても、開発とビルドから vscode + golang拡張 + go mod tidy は逃げられない。
しかもgolang用vscode拡張は開箱即戦力ではなく、入れたあとも dlv などを別途入れる。
互換性
互換性は大きくOS互換と言語の前方互換に分かれる。
golangが前方互換でない例
あまり知られていないが、golangは長年のあいだに前方互換を壊す変更を出している:
| Go バージョン | 分類 | 非互換の内容 | 回避・備考 |
|---|---|---|---|
| Go 1 (2012) | 言語・標準庫の大改修 | pre-Go 1 (r60 等) からの巨大差分:パス変更(例: encoding/asn1)、os.Error → error、time再設計、map/delete、rune、マップ走査のランダム化など |
go fix で移行。最大の breaking 更新。 |
| Go 1.1 | 言語・プラットフォーム | 定数ゼロ除算がエラー;64bit で int/uint が64bit;一部 net/syscall の型・シグネチャ変更 |
コンパイル・実行に直撃。 |
| Go 1.5 | ランタイム | GOMAXPROCS 既定が 1 → CPU 数 |
並行・性能の前提が変わる。 |
| Go 1.21 | ランタイム・panic | panic(nil) / 型なし nil が *runtime.PanicNilError を panic(以前は panic しない) |
GODEBUG=panicnil=1 または go 1.20 以前。 |
| Go 1.21 | パッケージ初期化 | 初期化順が規定(import path 順);以前は未定義 | 暗黙の初期化順に依存したコードに影響。 |
| Go 1.22 | 言語(for) |
ループ変数が反復ごとに新規(以前は共有)。クロージャの捕獲が変わる | go.mod の go 1.22 以上で有効。よくある移行ポイント。 |
| Go 1.22 | net/http.ServeMux |
メソッド接頭辞(POST /path)、{name} ワイルドカード、エスケープ変更 |
GODEBUG=httpmuxgo121=1 で Go 1.21 挙動。 |
| Go 1.22 | go/types |
型エイリアスが Alias として表現(以前は下位型と同一扱い) |
GODEBUG=gotypesalias=0(1.22 既定);1.23 から既定1、1.27 で削除予定。 |
| Go 1.22 | TLS・crypto | 最小 TLS 1.2;RSA KEX や 3DES など削除(以降も継続) | 複数の GODEBUG、一部 1.27 で削除。 |
| Go 1.23 | time |
time が作る channel がアンバッファ;Timer.Stop 等の正しい使い方に影響 |
GODEBUG=asynctimerchan=0(1.27 で削除予定)。 |
| Go 1.23 | net/http |
エラー応答で http.ServeContent が一部 header を除去 |
GODEBUG=httpservecontentkeepheaders=1。 |
| Go 1.23 | x509・TLS |
負のシリアル拒否;Leaf の扱い変更など |
x509negativeserial=0 等。 |
| Go 1.24 | x509 |
Certificate Policies フィールドの扱い | GODEBUG=x509usepolicies=0。 |
| Go 1.25 | ランタイム・nil チェック | 一部 nil デリファレンス(例: f, err := os.Open(); f.Name() で f==nil)が即 panic(以前は遅延することがあった) |
GODEBUG なし。err を先に確認。 |
| Go 1.25+ | プラットフォーム | 古い OS サポート廃止(例: macOS 11、32-bit windows/arm);Wasm エクスポート変更 | API 以外だがビルドに影響。 |
新しいgolangで古いgolangが生成した証明書を読むと、x509の変更でパースに失敗することがある。
互換性のもう一つの顔はツールチェーンの分断で、protoまわりが特に顕著だ。
1
2
3
4
5
6
7
8
9
10
11
12
13
# コア
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
# バリデーション
go install github.com/envoyproxy/protoc-gen-validate@latest
# HTTP ゲートウェイ
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@latest
# API ドキュメント
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@latest
go install github.com/google/gnostic/cmd/protoc-gen-openapi@latest
こんなに入れても、古いgolangに下げると dlv すら動かない。
互換バージョンを探して、ツールの海で迷子になる。
常に最新のgolangとは限らないので、作業フローは奇妙になる:
windowでpathをいじって古いgolangに切り替え → デバッグスタック非対応 → 対応版のツール ABCD を探す → A、次 B、次 C… のループ。
本当はプログラムを動かしたいだけなのに、go install のバージョンいじりに時間を焼く。
windowはgolang開発に向かない
windowではサードパーティツールの多くがビルドすら通らず、go installが笑い話になる。
なのでgolang開発は、なるべくLinux/macを使う流れになる。
選択的コンパイルの欠如
Goには C# や C 系のようなプリプロセッサ級の条件コンパイルがない:構文のなかで #if によって選ばれないビルドから丸ごと削る、というやり方はできない。近いのは build tags(//go:build)でファイルを分けるか、runtime.GOOS などの分岐で、意味も手触りも違う。C# 出身だと「選択的コンパイル」が足りないと感じる。
C# でよくある例(概略):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 1) シンボルでブロックを切る(DEBUG は Debug 構成で定義)
#if DEBUG
System.Diagnostics.Debug.WriteLine("Debug ビルドにだけ存在");
#endif
#if MY_FEATURE
// csproj の <DefineConstants> かコンパイラで MY_FEATURE を定義
DoExperimentalStuff();
#endif
// 2) Conditional: シンボル未定義なら呼び出し側が削られる(メソッド自体は残り得る)
using System.Diagnostics;
class App
{
[Conditional("VERBOSE")]
static void VerboseLog(string msg) => Console.WriteLine(msg);
static void Main()
{
VerboseLog("VERBOSE 未定義なら呼び出し命令は生成されない");
}
}
対してGoは「ビルドごとに実装を差し替える」なら、だいたいファイル単位の条件(//go:build linux)かリンク時の工夫で、関数の半分を #if で畳むことはしない。
その手のタグを使わないなら、環境変数で切り替える以外、すぐには思いつかない。
あとがき
ここまで書いて、AI時代に技術を共有することにまだ意味があるのか、自分に問う。
入門した頃、先輩はブログに問題と解を書けと言った。その習慣は10年、途切れ途切れ続いている。
いまはAI時代で、人間の学習速度はAIに追いつかない。それでも批判的思考のおかげでAIに流されにくく、AIは計算の補助だと割り切っている。
最終的なリスクは自分が負う。長年の経験で、運命は自分の手で握る以外、外物に引きずられる。
だから技術ブログそのものに意味はない。考えること自体が意味で、まとめるのはプロセスに過ぎず、記事は後世が懐かしむ結果にすぎない。
В двух словах: golang — это высокопроизводительный язык для сетевого программирования, не больше.
Ловушки нулевых значений
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
type AllBasicTypes struct {
B bool
Str string
I8 int8
I16 int16
I32 int32
I64 int64
I int
U8 uint8
U16 uint16
U32 uint32
U64 uint64
U uint
Up uintptr
By byte // uint8
Ru rune // int32
F32 float32
F64 float64
C64 complex64
C128 complex128
}
// var v AllBasicTypes — нулевые значения полей: false, "", 0, 0.0, (0+0i) и т.д.
Чтобы не ловить «как в Java» проблемы с нулевыми указателями, в golang принята модель нулевых значений по умолчанию. Поэтому при проектировании с нулевыми значениями нужно быть осторожным.
Для структуры выше после var v AllBasicTypes поле v.B даёт false, а v.U8 — 0.
В реальных сценариях непонятно: B это именно false или поле не задано. Обычно добавляют отдельную карту «установлено ли» или используют странный тип *bool.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import (
"fmt"
"time"
"google.golang.org/protobuf/types/known/timestamppb"
)
func main() {
// Плохой пример: нулевое значение time.Time
var zeroTime time.Time // или time.Time{}
protoTs := timestamppb.New(zeroTime)
// Сериализация падает
data, err := proto.Marshal(protoTs)
fmt.Println(err) // например: proto: Google.Protobuf.Timestamp.Seconds out of range -62135596800
}
Ошибка из-за нулевого времени и proto Timestamp почти незаметна.
Внизу стека вы могли объявить поле как time.Time, а на верхнем уровне API десериализация уже не проходит.
Слабая сеть
При плохой сети golang неудобен: стандартная библиотека скромная, постоянно нужен go get, а в Китае без прокси go get почти нереалистичен.

Даже если сеть и ромбовидные зависимости третьих сторон «решены», при слабом канале golang всё равно мучителен. Исходники скачал — а разработка и сборка всё равно упираются в vscode, расширение golang, go mod tidy.
Расширение для vscode не «из коробки»: поставил — и всё равно ставишь dlv и прочее.
Совместимость
Совместимость делится на совместимость ОС и совместимость языка вперёд по версиям.
Когда golang не совместим вперёд
Мало кто знает, что за годы в golang были несовместимые вперёд изменения:
| Версия Go | Категория | Что сломалось | Обход / примечание |
|---|---|---|---|
| Go 1 (2012) | Язык и стандартная библиотека | Огромный разрыв с pre-Go 1 (r60 и т.д.): пути пакетов (напр. encoding/asn1), os.Error → error, переработка time, map/delete, rune, случайный порядок обхода map и т.д. |
go fix. Крупнейшая ломающая миграция. |
| Go 1.1 | Язык и платформы | Деление константы на ноль — ошибка; на 64-bit int/uint 64-битные; часть net/syscall. |
Прямой эффект на сборку и выполнение. |
| Go 1.5 | Рантайм | GOMAXPROCS по умолчанию 1 → число CPU. |
Меняются допущения про параллелизм. |
| Go 1.21 | Рантайм и panic | panic(nil) / нетипизированный nil паникует *runtime.PanicNilError (раньше не паниковал). |
GODEBUG=panicnil=1 или go 1.20 и ниже. |
| Go 1.21 | Инициализация пакетов | Порядок инициализации задан (по import path); раньше — не определён. | Код, завязанный на неявный порядок, может сломаться. |
| Go 1.22 | Язык (for) |
Переменная цикла на каждую итерацию (раньше одна на все). Меняется захват в замыканиях. | Включается go 1.22+ в go.mod; частая точка миграции. |
| Go 1.22 | net/http.ServeMux |
Префиксы методов (POST /path), шаблоны {name}, экранирование. |
GODEBUG=httpmuxgo121=1 — поведение Go 1.21. |
| Go 1.22 | go/types |
Псевдонимы типов как Alias (раньше совпадали с основным типом). |
GODEBUG=gotypesalias=0 (по умолчанию в 1.22); с 1.23 по умолчанию 1; уберут в 1.27. |
| Go 1.22 | TLS и crypto | Минимум TLS 1.2; выкидывание RSA KEX, 3DES и т.д. | Несколько флагов GODEBUG, часть снимут в 1.27. |
| Go 1.23 | time |
Каналы из time — небуферизованные; влияет на корректный Timer.Stop. |
GODEBUG=asynctimerchan=0 (уберут в 1.27). |
| Go 1.23 | net/http |
http.ServeContent убирает часть заголовков при ошибках. |
GODEBUG=httpservecontentkeepheaders=1. |
| Go 1.23 | x509 и TLS |
Отрицательные серийные номера; изменения с полем Leaf и т.д. |
x509negativeserial=0 и др. |
| Go 1.24 | x509 |
Политики сертификатов (Policies). | GODEBUG=x509usepolicies=0. |
| Go 1.25 | Рантайм и nil | Часть разыменований nil (напр. f, err := os.Open(); f.Name() при f==nil) паникует сразу (раньше иногда откладывалось). |
Без GODEBUG; чинить код (сначала err). |
| Go 1.25+ | Платформы | Снятие поддержки старых ОС (напр. macOS 11, 32-bit windows/arm); изменения Wasm. | Влияет на сборку/порты. |
Разбор сертификатов, сгенерированных старым golang, новым компилятором может падать из-за изменений в x509.
Другая сторона — раскол инструментов; с proto это особенно заметно.
1
2
3
4
5
6
7
8
9
10
11
12
13
# База
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
# Валидация
go install github.com/envoyproxy/protoc-gen-validate@latest
# HTTP-шлюз
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@latest
# Документация API
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@latest
go install github.com/google/gnostic/cmd/protoc-gen-openapi@latest
Столько всего поставил — а на старом golang даже dlv не стартует.
Ищешь совместимые версии и теряешься в наборе плагинов.
Не всегда можно сидеть на последнем golang, поэтому цепочка действий становится странной:
Меняешь path на window на старый golang → стек отладки не подходит → ищешь инструменты A B C D под версию → починил A, потом B, потом C, по кругу.
Ты хотел просто запустить программу, а время уходит на go install с версиями.
window плохо подходит под golang
На window много сторонних утилит не собирается; go install превращается в анекдот.
Поэтому для golang чаще уходят на Linux / mac.
Нет условной компиляции в стиле препроцессора
У Go нет препроцессорной условной компиляции как у C# / C: #if, вырезающего целые куски из несобранных конфигураций. Ближе всего — build tags (//go:build) по разным файлам или ветки runtime.GOOS; смысл и эргономика другие. С фона C# явно чувствуешь отсутствие «выборочной компиляции».
Типичные приёмы в C# (схематично):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 1) Обрезка кода по символу (DEBUG задаётся в Debug)
#if DEBUG
System.Diagnostics.Debug.WriteLine("Только в Debug-сборке");
#endif
#if MY_FEATURE
// MY_FEATURE — в csproj <DefineConstants> или флагах компилятора
DoExperimentalStuff();
#endif
// 2) Conditional: если символ не задан, вызовы вырезаются (метод может остаться)
using System.Diagnostics;
class App
{
[Conditional("VERBOSE")]
static void VerboseLog(string msg) => Console.WriteLine(msg);
static void Main()
{
VerboseLog("Без VERBOSE вызов не попадёт в IL");
}
}
Для Go смена реализации между сборками — обычно условия на уровне файлов (//go:build linux) или трюки линковки, а не #if на полфункции.
Без таких тегов, кроме переменных окружения, другого аккуратного варианта я не вижу.
Постскриптум
Пишу это и спрашиваю себя: есть ли смысл делиться технологиями в эпоху ИИ?
Когда я начинал, советовали вести блог с проблемами и решениями. Так я делаю с перерывами уже десять лет.
Скорость обучения людей не догоняет ИИ — но критическое мышление не даёт увлечься потоком; ИИ для меня вспомогательный «калькулятор».
Риск всё равно на мне. Годы показали: судьбу держишь в своих руках — или тебя волокут обстоятельства.
Значит сам пост не цель. Смысл — в размышлении; систематизация — процесс; статья — лишь след для тех, кто потом вспомнит.
💬 讨论 / Discussion
对这篇文章有想法?欢迎在 GitHub 上发起讨论。
Have thoughts on this post? Start a discussion on GitHub.