Task: Go 编写的现代任务运行器¶
为什么要学 Task¶
每个项目都需要一套构建/测试/部署命令。传统做法是用 Makefile,但 Make 有很多历史包袱:Tab vs 空格敏感、平台兼容性差、语法晦涩。npm scripts 又依赖 Node.js 生态。
Task(又叫 Taskfile)是一个用 Go 编写的任务运行器,使用 YAML 定义任务。它的目标是做一个简单、跨平台、零依赖的 Make 替代品。
| 维度 | Make | npm scripts | Task |
|---|---|---|---|
| 语法 | Makefile (晦涩) | JSON (受限) | YAML (直观) |
| 跨平台 | 差 (需要 GNU Make) | 需要 Node.js | Go 单二进制 |
| 依赖管理 | 文件级依赖 | 无 | 任务级依赖 |
| 变量/环境 | 复杂 | 受限 | 简洁的 .env 支持 |
| 增量运行 | 文件时间戳 | 无 | 文件 hash + 时间戳 |
| Watch 模式 | 需要额外工具 | 需要 nodemon | 内置 |
| 并行执行 | 有限 | 需要 concurrently | 原生支持 |
| 命令补全 | 无 | 无 | 内置 |
核心概念¶
白话解释¶
Task 就是一个"命令管家"。你在 Taskfile.yml 中定义一系列命名的任务,每个任务是一组要执行的命令。然后通过 task 任务名 来运行。
它比手动敲命令的优势在于: - 命令有名字,不用记长串参数 - 任务之间可以声明依赖关系 - 支持文件变化检测,避免重复执行 - YAML 格式,任何人都能读懂
核心概念表¶
| 概念 | 说明 | 示例 |
|---|---|---|
| Task | 一个命名的命令集合 | build, test, deploy |
| Dep | 任务依赖,先执行依赖再执行自身 | deps: [lint, test] |
| Cmd | 要执行的 Shell 命令 | cmds: ["go build ."] |
| Var | 变量定义 | vars: { NAME: "app" } |
| Env | 环境变量 | env: { GO111MODULE: "on" } |
| Source/Generates | 增量运行的文件检测 | sources: ["**/*.go"] |
| Precondition | 前置条件检查 | preconditions: [test -f config.yml] |
| Include | 引入其他 Taskfile | includes: { docker: ./docker/Taskfile.yml } |
| Internal | 内部任务(不在列表中显示) | internal: true |
安装配置¶
安装方式¶
macOS
Linux
# 通用安装脚本
sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d -b /usr/local/bin
# Snap
sudo snap install task --classic
# Arch Linux
pacman -S go-task
Go 安装
Windows
Shell 补全¶
# Bash
echo 'eval "$(task --completion bash)"' >> ~/.bashrc
# Zsh
echo 'eval "$(task --completion zsh)"' >> ~/.zshrc
# Fish
task --completion fish | source
# 或持久化
task --completion fish > ~/.config/fish/completions/task.fish
第一个 Taskfile¶
在项目根目录创建 Taskfile.yml:
# Taskfile.yml
version: '3'
tasks:
hello:
desc: "打个招呼"
cmds:
- echo "Hello, World!"
build:
desc: "构建项目"
cmds:
- go build -o bin/app .
test:
desc: "运行测试"
cmds:
- go test ./...
default:
desc: "默认任务"
cmds:
- task --list
快速上手¶
任务依赖¶
version: '3'
tasks:
lint:
desc: "代码检查"
cmds:
- golangci-lint run
test:
desc: "运行测试"
deps: [lint] # 先执行 lint
cmds:
- go test -v ./...
build:
desc: "构建"
deps: [test] # 先执行 test(test 又会先执行 lint)
cmds:
- go build -o bin/app .
# 并行执行依赖
ci:
desc: "CI 流水线"
deps:
- lint
- test
- build:docs
cmds:
- echo "CI passed!"
变量和环境变量¶
version: '3'
vars:
APP_NAME: myapp
VERSION:
sh: git describe --tags --always
GO_FLAGS: -ldflags "-X main.version={{.VERSION}}"
env:
CGO_ENABLED: '0'
tasks:
build:
desc: "构建 {{.APP_NAME}}"
cmds:
- go build {{.GO_FLAGS}} -o bin/{{.APP_NAME}} .
docker:
desc: "构建 Docker 镜像"
vars:
IMAGE_TAG: '{{.APP_NAME}}:{{.VERSION}}'
cmds:
- docker build -t {{.IMAGE_TAG}} .
- echo "Built {{.IMAGE_TAG}}"
.env 文件支持¶
version: '3'
dotenv: ['.env', '.env.local']
tasks:
serve:
desc: "启动开发服务器"
cmds:
- echo "Starting on port $PORT"
- go run . -port $PORT
带参数的任务¶
version: '3'
tasks:
greet:
desc: "问候某人"
cmds:
- echo "Hello, {{.CLI_ARGS}}!"
migrate:
desc: "运行数据库迁移"
cmds:
- goose {{.CLI_ARGS}}
docker:run:
desc: "运行指定的 Docker 容器"
cmds:
- docker run -it {{.IMAGE}}
requires:
vars: [IMAGE]
task greet -- World
# → Hello, World!
task migrate -- up
# → goose up
task docker:run IMAGE=nginx
# → docker run -it nginx
进阶用法¶
增量运行(Sources/Generates)¶
version: '3'
tasks:
build:
desc: "构建(仅源文件变化时)"
sources:
- "**/*.go"
- go.mod
- go.sum
generates:
- bin/app
cmds:
- go build -o bin/app .
css:
desc: "编译 CSS"
sources:
- "src/**/*.scss"
generates:
- "dist/style.css"
cmds:
- sass src/main.scss dist/style.css
# 使用 checksum 方法(基于文件内容 hash)
proto:
desc: "编译 protobuf"
method: checksum # 默认是 timestamp
sources:
- "proto/**/*.proto"
generates:
- "gen/**/*.go"
cmds:
- protoc --go_out=gen/ proto/*.proto
Watch 模式¶
version: '3'
tasks:
dev:
desc: "开发模式(文件变化自动重启)"
watch: true
sources:
- "**/*.go"
cmds:
- go run .
test:watch:
desc: "持续运行测试"
watch: true
sources:
- "**/*.go"
- "**/*_test.go"
cmds:
- go test ./...
前置条件¶
version: '3'
tasks:
deploy:
desc: "部署到生产"
preconditions:
- sh: test -f .env.production
msg: "缺少 .env.production 文件"
- sh: git diff --quiet
msg: "有未提交的更改,请先提交"
- sh: '[ "$(git branch --show-current)" = "main" ]'
msg: "只能从 main 分支部署"
cmds:
- echo "Deploying..."
- ./deploy.sh
任务分组(Include)¶
# Taskfile.yml(主文件)
version: '3'
includes:
docker: ./taskfiles/Docker.yml
db: ./taskfiles/Database.yml
test:
taskfile: ./taskfiles/Test.yml
dir: ./tests # 在指定目录下执行
tasks:
setup:
desc: "初始化项目"
cmds:
- task: docker:up
- task: db:migrate
- task: test:unit
# taskfiles/Docker.yml
version: '3'
tasks:
up:
desc: "启动 Docker 容器"
cmds:
- docker compose up -d
down:
desc: "停止 Docker 容器"
cmds:
- docker compose down
并行执行¶
version: '3'
tasks:
# 依赖并行执行
ci:
deps:
- task: lint
- task: test:unit
- task: test:integration
# 命令并行执行
dev:
desc: "同时启动前后端"
cmds:
- task: backend
- task: frontend
parallel: true
backend:
internal: true
cmds:
- go run ./cmd/server
frontend:
internal: true
dir: ./frontend
cmds:
- npm run dev
条件执行¶
version: '3'
tasks:
install:
desc: "安装依赖"
status:
- test -d node_modules
cmds:
- npm install
build:linux:
desc: "Linux 构建"
platforms: [linux]
cmds:
- make build
build:mac:
desc: "macOS 构建"
platforms: [darwin]
cmds:
- make build
实际项目 Taskfile 示例¶
# 一个 Go Web 项目的完整 Taskfile
version: '3'
dotenv: ['.env']
vars:
APP_NAME: myapi
VERSION:
sh: git describe --tags --always 2>/dev/null || echo "dev"
LDFLAGS: -ldflags "-X main.version={{.VERSION}}"
tasks:
default:
cmds:
- task --list-all
setup:
desc: "项目初始化"
cmds:
- go mod download
- cp -n .env.example .env || true
- task: db:migrate
dev:
desc: "开发模式"
watch: true
sources:
- "**/*.go"
- ".env"
cmds:
- go run {{.LDFLAGS}} ./cmd/server
build:
desc: "构建生产版本"
deps: [lint, test]
sources:
- "**/*.go"
- go.mod
generates:
- bin/{{.APP_NAME}}
env:
CGO_ENABLED: '0'
cmds:
- go build {{.LDFLAGS}} -o bin/{{.APP_NAME}} ./cmd/server
test:
desc: "运行测试"
cmds:
- go test -race -coverprofile=coverage.out ./...
- go tool cover -func=coverage.out
lint:
desc: "代码检查"
cmds:
- golangci-lint run ./...
db:migrate:
desc: "数据库迁移"
cmds:
- goose -dir migrations postgres "$DATABASE_URL" up
db:rollback:
desc: "回滚迁移"
cmds:
- goose -dir migrations postgres "$DATABASE_URL" down
docker:build:
desc: "构建 Docker 镜像"
cmds:
- docker build -t {{.APP_NAME}}:{{.VERSION}} .
docker:run:
desc: "运行 Docker 容器"
deps: [docker:build]
cmds:
- docker run -p 8080:8080 --env-file .env {{.APP_NAME}}:{{.VERSION}}
clean:
desc: "清理构建产物"
cmds:
- rm -rf bin/ coverage.out
常见问题¶
Q1: Task 和 Make 该选哪个?¶
- 新项目:用 Task(YAML 更易读,跨平台更好)
- 已有 Makefile 的项目:不必迁移(Make 完全够用)
- C/C++ 项目:Make 的文件级依赖更适合编译链
- 跨平台项目:Task(避免 GNU Make 在 Windows 上的问题)
Q2: 命令中的 Shell 语法在 Windows 上不兼容?¶
Task 默认使用系统 Shell(Linux/Mac 是 sh,Windows 是 cmd)。建议: - 用 Go 模板语法代替 Shell 特性 - 或在 Taskfile 中指定 Shell:set: [pipefail]
Q3: 如何传递动态参数?¶
Q4: Taskfile 支持中文任务名吗?¶
支持,但不推荐。任务名是命令行参数,中文输入不方便:
Q5: 如何在多个项目之间共享 Taskfile?¶
使用 includes 从公共位置引入:
参考资源¶
| 资源 | 链接 |
|---|---|
| 官方网站 | https://taskfile.dev |
| GitHub 仓库 | https://github.com/go-task/task |
| 配置参考 | https://taskfile.dev/reference/schema |
| 使用指南 | https://taskfile.dev/usage |
| API 参考 | https://taskfile.dev/api |
| 示例集合 | https://taskfile.dev/community |
总结:Task 是 Make 的现代化替代品。YAML 语法让任何人都能读懂构建配置,内置的 watch 模式和增量运行减少了对额外工具的依赖。如果你正在开始一个新项目,或者厌倦了 Makefile 的语法怪癖,Task 是一个值得采用的工具。