Skip to content
最佳实践与陷阱

最佳实践与陷阱

全手册陷阱汇总与最佳实践清单,生产环境写 Makefile 前过一遍。


常见陷阱汇总

1. 🚨 Tab 与空格混用

build:
	go build ./...       # ← 这行开头是 Tab ✅

build:
    go build ./...      # ← 这行开头是 4 个空格 ❌
  • 症状*** missing separator. Stop.*** recipe commences before first target. Stop.
  • 根因:recipe 必须以 Tab 开头,这是 Make 最古老也最高频的坑
  • 排查:用 cat -A Makefile 看行首,Tab 显示为 ^I

💡 用支持「显示空白字符」的编辑器(VSCode 里 View: Render Whitespace),或配置 Makefile 强制插入 Tab。

2. 🚨 递归展开 = 导致意外求值 / 无限递归

CFLAGS = $(CFLAGS) -O2      # 自我引用 → 无限递归
X = $(Y)                    # 若 Y 在后面定义,值为后面的值(可能出乎意料)
  • 建议:默认用 :=(简单展开),只在确实需要延迟求值时用 =

3. 🚨 动作类目标忘了 .PHONY

build:                      # 若目录里恰好有个 build 文件
	go build ./...
  • 症状make build 输出 make: 'build' is up to date. 然后啥也不干
  • 根因:Make 把 build 当成文件,发现它存在且比依赖新,直接跳过
  • 修复:所有非文件产物目标都加进 .PHONY

4. 🚨 recipe 里 shell 变量未转义

for f in $(FILES); do \
	echo $f; \          # ❌ make 会先尝试展开 $f(空)
done
  • 正确:shell 变量写成 $$f,Make 变量才是 $(f)
  • 同样,$()$@ 等符号在 recipe 里若要传给 shell 原样使用,需写成 $$()

5. 🚨 变量名含空格 / 路径含空格

BIN = my app              # ❌ 变量值 "my app" 会被当两个词
  • Make 按空格分词,含空格的路径要格外小心。项目路径避免空格,变量值必要时用引号包住再在 recipe 中使用。

6. 🚨 递归 make 没用 $(MAKE)

sub:
	cd sub && make        # ❌ 丢失 -j、-n 等标志
  • 正确$(MAKE) -C sub,让子 make 继承当前 make 的所有标志。

7. 🚨 条件判断求值时机误解

VERSION := dev
ifeq ($(VERSION),prod)
	...
endif
VERSION := prod           # ❌ 上面 ifeq 不会因此重算
  • ifeq/ifdef解析阶段求值,只认当时的变量值。要按运行时值分支,应放到 recipe 里用 shell 的 if,或用 $(if ...) 函数。

8. 🚨 每行 recipe 是独立 shell

build:
	cd sub                 # ❌ 这一行 cd 后,下一行又回到原目录
	go build ./...
  • 根因:默认每行 recipe 在独立 shell 中执行,cd 不跨行生效
  • 解法:同一逻辑写一行 cd sub && go build,或用 \ 续行,或用 .ONESHELL: 让整段在一个 shell 里跑。

9. 🚨 @- 用错位置

build:
	@go build ./...       # @ 隐藏命令本身,不隐藏错误
	-rm -f app            # - 忽略失败,但错误信息仍会显示
  • @ 只隐藏「命令回显」,不吞掉输出或错误;- 只忽略「非零退出码」,错误信息照样打印。别指望它们当「静默开关」。

10. 🚨 环境变量悄悄覆盖

# 用户环境里 export 了 GOOS=windows,Makefile 里又没定义
# 则 $(GOOS) 意外等于 windows
  • Make 会把环境变量自动导入为变量。关键变量在 Makefile 顶部显式定义(或用 ?=),避免被环境「偷袭」。

最佳实践清单

  • .DEFAULT_GOAL := help,让裸 make 安全且友好
  • 动作类目标统一声明 .PHONY
  • 变量集中顶部,默认 := 赋值,用 ?= 提供可覆盖的默认值
  • 每个 target 配 ## target: 说明 注释,交给 help 自动生成
  • 复杂逻辑下沉 scripts/*.sh,Makefile 保持「薄」
  • 递归调用用 $(MAKE),目录切换用 -C
  • recipe 里 shell 变量用 $$ 转义
  • 版本/commit/日期通过 -ldflags 注入二进制
  • $(wildcard) 收集文件,避免 shell 通配符的不一致
  • -n(dry-run)与 -B(强制)用于调试
  • 提供 cleanfmtlinttest 等标准目标,形成统一习惯

生产检查清单

上线前对照自查:

  • make help 能正确列出所有目标且说明准确
  • make buildmake clean && make build 结果一致(无残留状态)
  • 多平台/多环境通过变量切换,而非复制多份 Makefile
  • 构建产物版本信息(version/commit/date)已注入,便于线上溯源
  • .PHONY 覆盖了所有动作目标,不存在被同名文件误跳过的隐患
  • recipe 中的 $$ 转义、$(MAKE)-C 用法正确
  • 在目标环境(含 Windows Git Bash / WSL)实际跑通过一次

调试速查表

现象 可能原因 排查命令
missing separator 用了空格而非 Tab cat -A Makefile
up to date 但不执行 .PHONY / 时间戳 make -B target
命令没回显 忘了加 @ 去掉 @ 重跑
变量值不对 展开时机 / 环境覆盖 make --debug=v
命令没执行 依赖不满足 / 目标已最新 make -n 看计划
递归 make 行为异常 没用 $(MAKE) 改为 $(MAKE) -C