Go新手实战常见三大坑:一是go run找不到main包,需将main.go置于cmd/appname/目录并确保无构建约束;二是HTTP路由404,须检查中间件注册顺序、handler签名匹配及路径斜杠;三是goroutine阻塞,主因未配对channel收发或缺超时控制。
Go新手跑第一个实战项目,八成会卡在几个固定环节:不是 go run 报错找不到包,就是 HTTP 服务起来但访问 404,又或者 goroutine 看似跑起来了却没输出、内存悄悄涨上去——这些问题不难,但排查路径模糊,容易反复折腾。
cmd/ 目录结构和入口文件命名很多新手按教程建了 main.go,却放在项目根目录或 pkg/ 下,go run 就会报 no Go files in current directory 或 cannot find package。Go 要求可执行程序必须有 package main 且含 func main(),同时 go run 默认只扫描当前目录下的 .go 文件。
cmd//main.go(例如 cmd/api/main.go),再用 go run cmd/api/main.go
go.mod),确保 main.go 所在目录没有被 //go:build 或 // +build 条件编译排除internal/ 或 pkg/ 里放 main.go——这些目录本就不允许被外部引用,更不会被 go run 主动发现用 Gin、Echo 或 net/http 写接口时,明明写了 router.GET("/user", handler),curl 却返回 404,常见原因不是路由写错了,而是注册时机或中间件拦截了请求。
router.Use() 添加的中间件必须在 router.GET() 之前调用,否则中间件根本收不到请求func(*gin.Context),传成 func(http.ResponseWriter, *http.Request) 就静默失败GET "/users" 和 GET "/users/" 是两个不同路由;Gin 默认不自动重定向,需显式配置 router.RedirectTrailingSlash = true
这是新手并发项目里最隐蔽的坑:协程启了,日志没打,也不 panic,程序就卡住不动了。典型原因是 unbuffered channel 发送后没人接收,goroutine 永久阻塞在 ch 上。
make(chan int),优先考虑带缓冲通道:ch := make(chan int, 1),或加 select + default 防死锁go func() { ... }() 里涉及 channel 操作的,必须确保有对应接收方,且接收逻辑不能被提前 return 绕过context.WithTimeout 包裹 goroutine 执行逻辑,防止因下游依赖挂起导致整个协程组不可控func badExample() {
ch := make(chan int)
go func() {
ch <- 42 // 永远阻塞:没人从 ch 接收
}()
// 程序卡在这里,或直接退出,协程泄漏
}
func goodExample() {
ch := make(chan int, 1) // 缓冲为 1,发送不阻塞
go func() {
ch <- 42
}()
select {
case v := <-ch:
fmt.Println(v)
case <-time.After(1 * time.Second):
fmt.Println("timeout")
}
}
.env 加载时机与作用域本地调试时,os.Getenv("DB_HOST") 返回空字符串,但 .env 文件明明写了。问题往往出在加载顺序:Go 本身不自动读 .env,得靠第三方库(如 godotenv),而它必须在任何使用环境变量的代码之前调用。
cmd//main.go 最顶部(package main 下一行)就调用 godotenv.Load(),不要放在 init() 或某
个 config 包里godotenv.Load(".env.local") 这类显式指定路径时,路径是相对于当前工作目录(os.Getwd()),不是源码目录.env 文件默认不进容器,要么 COPY 进去,要么改用 docker-compose.yml 的 environment: 字段直接注入真正卡住新手的,往往不是语法不会,而是这些“看不见的约定”——目录结构、加载顺序、channel 配对、环境变量作用域。它们不报错,只让程序行为偏离预期。多看一眼 go list -f '{{.Dir}}' . 输出的当前包路径,比重写三遍 handler 更管用。