文章目录一、命令可以生成一个go的工程1.1 go mod tidy 下载依赖1.2 运行命令二、包机制详解2.1、同一个目录下的所有 .go 文件必须属于同一个包名2.2、普通包一般遵循目录名包名package main 是特例2.3.package/import本质三、根目录四、 多 go.mod 工程示例4.1、为什么要多个 go.mod4.2、典型结构示例4.3 实战示例4.4 本地开发go.work五、多 go.mod 工程示例2六、module/package语法6.1 模块名包含域名的用法6.2 module/package/import语法6.3 与java的包路径对比一、命令可以生成一个go的工程初始化一个包含可执行 main 方法的 Go 工程只需两条命令。下面我会分步说明并解释每条命令的作用。操作步骤1、创建并进入项目目录首先为你的项目创建一个独立的文件夹并进入该目录mkdirmyprojectcdmyproject2、初始化 Go 模块 (go mod init)这是关键的一步用于创建一个 Go 模块来管理你的项目依赖。运行以下命令go mod init模块名称模块名称通常是你的代码仓库的路径例如 github.com/yourusername/myproject 。如果是本地测试也可以使用一个简单的名称例如 myproject 。执行后会在当前目录生成一个go.mod文件这是项目依赖管理的核心文件3、创建 main.go 文件可以和go.mod文件平级catmain.goEOFpackagemainimportfmtfuncmain(){fmt.Println(Hello, World!)}EOFpackage main 声明了这是一个可独立执行的程序 。func main() 是程序的入口函数 。fmt.Println 用于在控制台打印输出如何运行完成以上步骤你就有两种方式来运行你的Go程序了命令作用适用场景go run .直接编译并运行程序不会在当前目录留下可执行文件。开发调试阶段频繁修改和测试时使用。go build .仅编译生成一个可执行的二进制文件Windows下为.exe文件。编译完成后需要分发或部署程序时使用。项目结构示意完成后你的项目目录结构看起来会像这样myproject/ ├── go.mod# 模块管理文件└── main.go# 主程序源文件1.1 go mod tidy 下载依赖如果你的go文件中Import了第三方包那么需要go mod tidy命令自动下载依赖1.2 运行命令特性go run main.gogo run .指定方式显式指定要运行的文件指定一个包当前目录的包处理文件范围只编译和运行main.go这一个文件编译并运行当前目录下整个包的所有.go文件多文件项目❌ 如果项目有其他文件如helper.go会因为未定义而编译失败✅ 自动包含包内所有文件正常工作假设你的项目结构如下myproject/├── main.go├── helper.go└── utils/└── utils.gomain.go:packagemainfuncmain(){printMessage()// 这个函数定义在 helper.go 中}helper.go:packagemainimportfmtfuncprintMessage(){fmt.Println(Hello from helper!)}运行 go run main.go# ❌ 会报错 #./main.go:4:2:undefined:printMessage因为 go run main.go 只编译 main.go编译器不知道 printMessage 函数存在。运行 go run .# ✅ 正常运行 # Hello from helper!因为 go run . 编译了当前包myproject下的所有 .go 文件main.go 和 helper.go所以能找到 printMessage 函数。go run . 的工作方式是解析当前目录下的包package main编译该包中的所有 .go 文件包括 main.go、helper.go 等找到其中的main() 函数作为入口点 一个目录下只允许有一个带有main函数的go文件运行生成的可执行文件二、包机制详解2.1、同一个目录下的所有 .go 文件必须属于同一个包名错误示例// 目录 /myapp/user/// user.gopackageuser// ❌ 错误// profile.gopackageprofile// ❌ 同一个目录下不同包名// 编译错误found packages user and profile in same directory正确示例// 目录 /myapp/user/// user.gopackageuser// ✅ 正确// profile.gopackageuser// ✅ 同一个包名这三个文件虽然在不同的文件中但因为同一个目录且同一个包名2.2、普通包一般遵循目录名包名package main 是特例假如我有一个工程用于学习go的代码每个目录代表学习一个语法特性每个目录下有一个main.go通过执行main.go来实现只有一个go.mod的情况下代码结构如下/myproject/├──go.mod # module myproject ├──/cmd/│ ├──/server/│ │ └── main.go#packagemain │ ├──/worker/│ │ └── main.go#packagemain │ └──/cli/│ └── main.go#packagemain └──/internal/└──/shared/├── config.go#packageshared └── logger.go#packageshared代码示例/cmd/server/main.gopackagemainimport(myproject/internal/shared)funcmain(){config:shared.LoadConfig()logger:shared.NewLogger()// 启动服务器逻辑logger.Info(Starting server on port 8080)// ...}/cmd/worker/main.gopackagemainimport(myproject/internal/shared)funcmain(){config:shared.LoadConfig()logger:shared.NewLogger()// 启动 worker 逻辑logger.Info(Starting worker)// ...}/internal/shared/config.gopackageshared// 注意不是 maintypeConfigstruct{Portint// ...}funcLoadConfig()*Config{returnConfig{Port:8080}}直接运行示例# 直接运行不保留可执行文件gorun./cmd/servergorun./cmd/workergorun./cmd/cli # 或者进入目录运行 cd./cmd/servergorun main.go关键点所有可执行入口必须是 package main并且包含 func main()共享代码用普通包名如 package shared、package auth、package dbimport 路径基于 go.mod 中的 module 名如果 go.mod 是 module myproject那么内部包导入就是 myproject/internal/shared同一个模块里可以有多个 package mainGo 不会冲突因为它们是不同的目录编译时各自独立main.go 的包名必须是 main无论它放在什么目录下/cmd/server/main.go→packagemain(目录名是 server)/cmd/worker/main.go→packagemain(目录名是 worker)/cmd/cli/main.go→packagemain(目录名是 cli)因为 Go 规定可执行程序的入口包必须叫 main这是硬性要求与目录名无关。为什么 main 包可以和目录不一致因为 main 包不会被其他包导入它只是程序的入口点。所以导入路径目录名对外部没有意义没有人会写 import “xxx/server” 来导入一个 main 包因此目录名可以任意包名固定为 main2.3.package/import本质结合前一小节的示例看下本质(专业术语可以参加6.2 module/package/import语法)技术上你可以这样写声明包骚操作这会让阅读代码的人非常困惑强烈不推荐。// /internal/shared/config.gopackagehelper//这是一个将来会被别人导入的 包名 helper但自己的目录是 shared导入时采用物理目录这是万年不变的地方importmyproject/internal/shared// 路径还是 shared使用时使用之前声明的包helper.LoadConfig()// 但要用 helper不是 shared打个比方一下导入的时候要找到文件目录就像是找到家庭住址门口有个牌子写着102室对应 import “myproject/internal/shared”在家里时你有个小名叫狗蛋对应包的概念对应 package helper当快递员敲门后会问狗蛋在家吗 如果命名时包和路径一致时此时只有大名或者说小名大名快递员会说xx街道102户主在家吗三、根目录Go 的根目录概念有两种别搞混了1、GOROOTGo 安装根目录Go 语言自己的家查看 GOROOT这个环境变量即可# 你安装 Go 的地方/usr/local/go/# Linux/Mac 默认C:\Go\# Windows 默认# 里面装着 Go 自己的东西/usr/local/go/ /src/# Go 标准库的源码fmt, net, http等/bin/# go 命令本身/pkg/# 编译好的库文件这个你不用管Go 自己会用。就像你不需要知道 Python 安装在哪里一样。2、Module 根目录现代 Go✅ 重点你项目的根目录由 go.mod 文件标记# 你的项目根目录/myapp/# ← 这就是项目根目录go.mod # ← 有这个文件的就是根目录 main.go/internal/# 从这个目录开始算/pkg//cmd/怎么知道根目录在哪里# 在项目任意目录执行go list-m# 输出myapp 这就是模块名# 或者找 go.mod 文件find.-namego.mod# 输出./go.mod 所在目录就是根目录四、 多 go.mod 工程示例一个项目可以有多个 go.mod 文件这叫 Multi-Module Repository。这在大型项目或微服务架构中很常见。4.1、为什么要多个 go.mod独立版本管理每个模块独立发版依赖隔离不同模块可以用不同版本的依赖微服务架构每个服务独立大型 Monorepo一个仓库包含多个项目4.2、典型结构示例/mycompany/# Git 仓库根目录├── go.mod# 根模块可选├── go.work# Workspace 文件本地开发用│ ├── /services/ │ ├── /user-service/ │ │ ├── go.mod# user-service 独立模块模块根│ │ ├── main.go │ │ └── /internal/ │ │ │ ├── /order-service/ │ │ ├── go.mod# order-service 独立模块模块根│ │ ├── main.go │ │ └── /internal/ │ │ │ └── /payment-service/ │ ├── go.mod │ └── main.go │ ├── /libs/ │ ├── /common/ │ │ ├── go.mod# 公共库模块│ │ └── /auth/ │ │ │ └── /database/ │ ├── go.mod │ └── mysql.go │ └── /tools/ └── /cli/ ├── go.mod# 工具模块└── main.go4.3 实战示例项目结构:ecommerce/# Git 仓库根目录 ├──go.work # Workspace本地开发 │ ├──/services/│ ├──/user-service/│ │ ├──go.mod │ │ ├── main.go│ │ └── user.go│ │ │ ├──/order-service/│ │ ├──go.mod │ │ ├── main.go│ │ └── order.go│ │ │ └──/api-gateway/│ ├──go.mod │ └── main.go│ └──/pkg/├──/common/│ ├──go.mod │ ├──/logger/│ │ └── logger.go│ └──/errors/│ └── errors.go│ └──/protos/├──go.mod └── user.pb.go项目准备:# 创建项目结构 mkdir-p ecommerce/{services/user-service,services/order-service,pkg/common}cd ecommerce # 创建各模块的go.mod cd services/user-servicegomod init ecommerce/services/user-service cd../order-servicegomod init ecommerce/services/order-service cd../../pkg/commongomod init ecommerce/pkg/common cd../..# 现在有了三个独立的模块单模块构建构建命令# 进入 user-service 模块cdservices/user-service# 下载依赖go mod download# 整理依赖go mod tidy# 构建当前模块go build ./...# 构建并生成可执行文件go build-obin/user-service ./cmd/server# 运行测试gotest./...# 运行当前模块go run main.go模块定义文件services/user-service/go.mod:module ecommerce/services/user-service # 模块名称供其他的go.mod文件通过require配置依赖go1.21require(ecommerce/pkg/common v0.0.0#通过共享库明确依赖声明依赖 common 模块 ecommerce/pkg/protos v0.0.0github.com/gorilla/mux v1.8.0)// 本地开发时指向本地路径生产环境会替换为真实版本replace(ecommerce/pkg/common../../pkg/common ecommerce/pkg/protos../../pkg/protos)services/order-service/go.mod:module ecommerce/services/order-service # 模块名称供其他的go.mod文件通过require配置依赖go1.21require(ecommerce/pkg/common v0.0.0#通过共享库明确依赖声明依赖 common 模块 ecommerce/pkg/protos v0.0.0github.com/go-redis/redis/v8 v8.11.5)replace(ecommerce/pkg/common../../pkg/common ecommerce/pkg/protos../../pkg/protos)pkg/common/go.mod:module ecommerce/pkg/common # 模块名称供其他的go.mod文件通过require配置依赖go1.21require(github.com/sirupsen/logrus v1.9.0)pkg/protos/go.mod:module ecommerce/pkg/protosgo1.21require(google.golang.org/grpc v1.58.0google.golang.org/protobuf v1.31.0)代码示例services/user-service/user.gopackagemainimport(ecommerce/pkg/common/logger# 模块名包名进行引用ecommerce/pkg/protosgithub.com/gorilla/mux)funcmain(){// 使用公共模块logger.Info(User service starting...)r:mux.NewRouter()// 使用 protos 定义的结构user:protos.User{Id:123,Name:Alice}// ...}services/order-service/order.go:packagemainimport(ecommerce/pkg/common/logger# 依赖 common 模块ecommerce/pkg/protos)funcmain(){// 同一个 common 包但可以有不同的版本logger.Info(Order service starting...)order:protos.Order{Id:ORD-001}// ...}ecommerce/pkg/common/logger/logger.go// ecommerce/pkg/common/logger/logger.gopackagelogger// ← 这是包名决定了外部如何使用import(fmttime)// 公开的常量大写开头const(LevelDebugDEBUGLevelInfoINFOLevelErrorERROR)// 公开的变量大写开头varDefaultLevelLevelInfo// 私有的变量小写开头varcurrentTimetime.Now// 公开的结构体大写开头typeLoggerstruct{prefixstringlevelstring}// 构造函数公开funcNew(prefixstring)*Logger{returnLogger{prefix:prefix,level:DefaultLevel,}}// 公开方法大写开头func(l*Logger)Info(msgstring){ifl.levelLevelInfo||l.levelLevelDebug{fmt.Printf([%s] [INFO] %s: %s\n,time.Now().Format(15:04:05),l.prefix,msg)}}func(l*Logger)Debug(msgstring){ifl.levelLevelDebug{fmt.Printf([%s] [DEBUG] %s: %s\n,time.Now().Format(15:04:05),l.prefix,msg)}}func(l*Logger)Error(msgstring){fmt.Printf([%s] [ERROR] %s: %s\n,time.Now().Format(15:04:05),l.prefix,msg)}// 公开的函数大写开头funcLogInfo(prefix,msgstring){fmt.Printf([INFO] %s: %s\n,prefix,msg)}funcLogError(prefix,msgstring){fmt.Printf([ERROR] %s: %s\n,prefix,msg)}// 私有的辅助函数小写开头funcformatMessage(level,prefix,msgstring)string{returnfmt.Sprintf([%s] %s: %s,level,prefix,msg)}4.4 本地开发go.work创建 workspace# 在项目根目录 ecommerce/go work init ./services/user-service ./services/order-service ./pkg/common# 查看 workspace 配置catgo.work# 输出# go 1.21## use (# ./services/user-service# ./services/order-service# ./pkg/common# )ecommerce/go.work解决本地开发时的模块替换问题go1.21use(./services/user-service./services/order-service./services/payment-service./services/api-gateway./pkg/common./pkg/protos)使用 Workspace 的好处# 不需要在每个模块里写 replace 指令 cd services/user-servicegorun main.go# 自动使用本地代码 # 修改 pkg/common 的代码 # user-service 会立即生效无需重新发布五、多 go.mod 工程示例2多级嵌套情况存在如下结构存在一个myrepo/go.mod 和myrepo/services//go.modmyrepo/├──go.mod # 模块根 A ├── dao/│ ├── orderdao.go# 查找结果模块 A │ └── sub/│ └── helper.go# 查找结果模块 A ├── services/│ ├──go.mod # 模块根 B │ └── auth/│ └── main.go# 查找结果模块 B被阻断后不会往上查找 └── tools/└── gen/└── main.go# 查找结果向上到根没有go.mod # 一直找到根目录模块 A 为示例给出不同构建命令的溯源过程当进行编译时myrepo/services/auth会寻找go.mod 那么怎么查找的呢cdmyrepo/services/auth go list-m# 输出myrepo/services 不是 myrepogoenvGOMOD# 输出/path/to/myrepo/services/go.mod查找过程# 1. 从 auth/ 开始# 2. 没找到 go.mod# 3. 向上到 services/# 4. 找到 go.mod停止不会继续向上# 结果使用 services/go.mod不是根目录的当进行编译时 myrepo/dao会寻找go.mod 那么怎么查找的呢cdmyrepo/dao go build# 查找过程# 1. 从 /myrepo/dao/ 开始# 2. 没有 go.mod# 3. 向上到 /myrepo/# 4. 找到 go.mod ✅停止# 结果使用 /myrepo/go.mod模块根 A一个模块可以包含任意深度的子目录只要这些子目录没有自己的 go.mod构建时会往上找查找从当前目录开始向上逐级找到第一个 go.mod 就停止如果一直找到根目录都没有报错子目录没有 go.mod 属于最近的上级模块六、module/package语法6.1 模块名包含域名的用法这个域名其实不是真正的网络域名而是模块的唯一标识符。让我用最简单的方式解释// 这些域名的作用不是让 Go 去访问网站module github.com/gin-gonic/gin// 只是一个名字module ecommerce/services/user// 也只是一个名字module mycompany.com/utils// 还是一个名字// Go 并不检查这个域名是否真实存在// 它的作用是确保全世界不会有另一个模块叫这个名字写法1无域名示例中的写法module ecommerce/services/user-service // 适用场景 // ✅ 本地学习项目 // ✅ 公司内部项目配合 replace 使用 // ✅ 快速原型开发写法2有域名生产推荐module github.com/mycompany/ecommerce/services/user-service // 适用场景 // ✅ 开源项目 // ✅ 对外发布的库 // ✅ 需要被其他项目远程引用的模块完整的对比示例# 示例中的写法本地开发ecommerce/ ├── services/ │ └── user-service/ │ ├── go.mod# module ecommerce/services/user-service│ └── main.go └── pkg/ └── common/ ├── go.mod# module ecommerce/pkg/common└── common.go# 生产环境的写法对外发布ecommerce/ ├── services/ │ └── user-service/ │ ├── go.mod# module github.com/mycompany/ecommerce/services/user-service│ └── main.go └── pkg/ └── common/ ├── go.mod# module github.com/mycompany/ecommerce/pkg/common└── common.go6.2 module/package/import语法下面提到的路径指的是物理目录module模块名 域名(可省略) 模块根路径模块的唯一标识即go.mod文件所在的全路径package包名 当前.go文件声明的包名与目录名无关只是惯例一般把包名定位父目录仅父目录一层import 被引用的模块名 从该模块根到目标包的相对路径含所有层# 文件系统/home/user/projects/myapp/ ├── go.mod# module example.com/myapp├── main.go └── services/ └── user/ └── user.go# package user# 对应关系文件系统路径 → /home/user/projects/myapp/services/user/user.go 模块名 → example.com/myapp 包路径导入路径的后半部分→ services/user 包名package声明 → user 完整导入路径 → example.com/myapp/services/user6.3 与java的包路径对比go在声明时简单一些用短路径导入和java差不多都要带上长路经否则无法区分相同的包名通过模块包来进行区分Java路径必须严格匹配// 文件路径src/main/java/com/mycompany/user/service/UserService.javapackagecom.mycompany.user.service;// 长路径包含所有父级publicclassUserService{publicvoidcreateUser(){...}}包名很长com.mycompany.user.service包含所有信息公司域名 项目名 模块名 子模块强制匹配目录结构必须完全一致Go不强制匹配声明短路径但强烈建议// 文件路径src/mycompany/user/service/user.gopackageservice// 短路径只有最后一段funcCreateUser(){// ...}包名很短service不含父级信息不关心谁包含它不强制匹配目录名和包名可以不同但建议一致