CppGuide社区 CppGuide社区
首页
  • 最新谷歌C++风格指南(含C++17/20)
  • C++17详解
  • C++20完全指南
  • C++23快速入门
  • C++语言面试问题集锦
  • 🔥C/C++后端开发常见面试题解析 (opens new window)
  • 网络编程面试题 (opens new window)
  • 网络编程面试题 答案详解 (opens new window)
  • 聊聊WebServer作面试项目那些事儿 (opens new window)
  • 字节跳动面试官现身说 (opens new window)
  • 技术简历指南 (opens new window)
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 从零用C语言写一个Redis
  • Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • Go语言特性

    • Go开发实用指南
    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
  • Rust编程

    • Rust编程指南
  • 数据库

    • SQL零基础指南
    • MySQL开发与调试指南
GitHub (opens new window)
首页
  • 最新谷歌C++风格指南(含C++17/20)
  • C++17详解
  • C++20完全指南
  • C++23快速入门
  • C++语言面试问题集锦
  • 🔥C/C++后端开发常见面试题解析 (opens new window)
  • 网络编程面试题 (opens new window)
  • 网络编程面试题 答案详解 (opens new window)
  • 聊聊WebServer作面试项目那些事儿 (opens new window)
  • 字节跳动面试官现身说 (opens new window)
  • 技术简历指南 (opens new window)
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 从零用C语言写一个Redis
  • Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • Go语言特性

    • Go开发实用指南
    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
  • Rust编程

    • Rust编程指南
  • 数据库

    • SQL零基础指南
    • MySQL开发与调试指南
GitHub (opens new window)
  • Go开发实用指南 说明
  • 第1章 Go项目如何组织
    • 模块和包
    • 技术要求
    • 创建模块
      • 操作方法
    • 创建源代码目录结构
      • 操作方法
    • 构建和运行程序
      • 操作方法……
    • 导入第三方包
      • 操作方法……
    • 导入特定版本的包
      • 操作方法……
    • 使用模块缓存
    • 操作方法……
    • 使用内部包减少API暴露面
      • 操作方法……
    • 使用模块的本地副本
      • 操作方法……
    • 处理多个模块——工作区(workspace)
      • 操作方法……
    • 管理模块版本
      • 操作方法……
    • 总结与延伸阅读
  • 第2章 字符串处理
  • 第3章 处理日期和时间
  • 第4章 使用数组、切片和映射
  • 第5章 使用类型、结构体和接口
  • 第6章 使用泛型
  • 第7章 并发
  • 第8章 错误与恐慌(panic)
  • 第9章 context包
  • 第10章 处理大量数据
  • 第11章 处理JSON数据
  • 第12章 进程
  • 第13章 网络编程
  • 第14章 流式输入/输出
  • 第15章 数据库
  • 第16章 日志记录
  • 第17章 测试、基准测试和性能分析
目录

第1章 Go项目如何组织

# 第1章 Go项目如何组织

本章主要介绍如何开启一个新项目、组织源代码目录结构以及管理开发程序所需的包。设计良好的项目结构非常重要,因为当其他开发者参与你的项目或尝试使用其中的组件时,他们能够快速、轻松地找到所需内容。本章首先会解答你在开启新项目时可能遇到的一些问题,然后探讨如何使用Go语言的包系统,如何使用标准库和第三方包,以及如何让其他开发者更方便地使用你的包。本章包含以下内容:

  • 创建模块
  • 创建源代码目录结构
  • 构建和运行程序
  • 导入第三方包
  • 导入特定版本的包
  • 使用内部包减少API暴露面
  • 使用模块的本地副本
  • 工作区
  • 管理模块版本

# 模块和包

首先,了解一些关于模块(module)和包(package)的知识会很有帮助。包是数据类型、常量、变量和函数的聚合单元。我们构建和测试的是包,而不是单个文件或模块。当构建一个包时,构建系统会收集并构建所有依赖包。如果包名为main,构建它将生成一个可执行文件。你可以运行main包而无需生成二进制文件(更具体地说,Go构建系统首先构建包,在临时位置生成二进制文件,然后运行它)。要使用另一个包,需要导入它。模块有助于在项目中组织多个包并解决包引用问题。一个模块就是多个包的集合。如果你在程序中导入一个包,包含该包的模块将被添加到go.mod文件中,并且该模块内容的校验和会被添加到go.sum文件中。模块还能帮助你管理程序的版本。

一个包的所有文件都存储在文件系统的单个目录下。每个包都有一个使用package指令声明的名称,包内所有源文件共享该名称。包名通常与包含这些文件的目录名匹配,但并非必须如此。例如,main包通常不在名为main/的目录下。包的目录决定了包的“导入路径”。你使用import <importPath>语句将另一个包导入到当前包中。导入一个包后,你可以使用该包的名称(不一定是目录名)来访问该包中声明的名称。

模块名指向模块内容存储在互联网版本控制系统中的位置。在撰写本文时,这并非硬性要求,所以实际上你可以创建不遵循此惯例的模块名。但为避免未来与构建系统可能出现的不兼容问题,应避免这样做。你的模块名应该是这些模块中包的导入路径的一部分。特别要注意的是,模块名的第一个部分(第一个/之前的部分)没有.的,是为标准库保留的。

这些概念如图1.1所示。

img

图1.1 模块和包

  1. go.mod文件中声明的模块名是可以找到该模块的存储库路径。
  2. main.go中的导入路径定义了导入包的位置。Go构建系统将使用此导入路径查找包,然后通过扫描包路径的父目录来定位包含该包的模块。找到模块后,它将被下载到模块缓存中。
  3. 导入模块中定义的包名是你用于访问该包符号的名称。它可能与导入路径的最后一个部分不同。在我们的示例中,包名是example,但该包的导入路径是github.com/bserdar/go-recipes-module。
  4. Example函数位于example包中。
  5. example包还导入了同一模块中包含的另一个包。构建系统会识别该包属于同一模块,并使用下载的模块版本来解析引用。

# 技术要求

你需要在计算机上安装较新版本的Go语言,才能构建和运行本章中的示例。本书中的示例使用Go 1.22版本进行测试。本章的代码可以在https://github.com/PacktPublishing/Go-Recipes-for-Developers/tree/main/src/chp1 (opens new window) 找到 。

# 创建模块

当你开始着手一个新项目时,首先要做的是为它创建一个模块。模块是Go语言管理依赖关系的方式。

# 操作方法

  1. 创建一个目录来存储新模块。
  2. 在该目录下,使用go mod init <moduleName>命令创建新模块。go.mod文件标记了模块的根目录。该目录下的任何包都将成为此模块的一部分,除非该目录也有一个go.mod文件。尽管构建系统支持这种嵌套模块,但使用它们并没有太多好处。
  3. 要导入同一模块中的包,使用moduleName/packagePath。当moduleName与模块在互联网上的位置相同时,就不会对所引用的内容产生歧义。
  4. 对于模块下的包,模块的根目录是包含go.mod文件的最近父目录。模块内对其他包的所有引用都将在模块根目录下的目录树中查找。
  5. 首先创建一个目录来存储项目文件。你当前的目录可以位于文件系统的任何位置。我见过有人使用一个通用目录来存储他们的工作,例如$HOME/projects(在Windows系统中是\user\myUser\projects)。你也可以选择使用与模块名相似的目录结构,比如$HOME/github.com/mycompany/mymodule(在Windows系统中是\user\myUser\github.com\mycompany\mymodule)。根据你的操作系统,你可能会找到更合适的位置。

警告

不要在Go安装目录的src/目录下工作。那是Go标准库的源代码。

提示

你不应该设置GOPATH环境变量;如果必须保留它,也不要在该路径下工作。这个变量用于旧的操作模式(Go版本<1.13),现在已被弃用,转而使用Go模块系统。

在本章中,我们将使用一个简单的程序,该程序在网页浏览器中显示一个表单,并将输入的信息存储到数据库中。

创建模块目录后,使用go mod init命令。以下命令将在projects目录下创建一个webform目录,并在其中初始化一个Go模块:

$ cd projects
$ mkdir webform
$ go mod init github.com/examplecompany/webform
1
2
3

这将在该目录下创建一个go.mod文件,内容如下:

module github.com/PacktPublishing/Go-Recipes-for-Developers/chapter1/webform
go 1.21.0
1
2

使用一个能够描述模块存储位置的名称。始终使用类似<host>.<domain>/location/to/module的URL结构(例如github.com/bserdar/jsonom)。特别要注意的是,模块名的第一个部分应该包含一个点(.)(Go构建系统会检查这一点)。

所以,尽管你可以将模块命名为webform或mywork/webform之类的名称,但不建议这样做。不过,你可以使用类似workspace.local/webform的名称。如果不确定,就使用代码存储库的名称。

# 创建源代码目录结构

创建好新模块后,就该考虑如何组织源文件了。

# 操作方法

根据项目的不同,有几种既定的惯例:

  • 使用标准布局,例如 https://github.com/golang-standards/project-layout (opens new window)。
  • 功能单一的库可以将所有导出的名称放在模块根目录,实现细节可选择存储在内部包中。生成单个可执行文件且可复用组件较少或没有的模块,也可以使用扁平目录结构。

对于像我们这样生成可执行文件的项目,https://github.com/golang-standards/project-layout (opens new window) 中规定的结构很适用。那么,让我们遵循这个模板:

webform/
	go.mod
	cmd/
		webform/
			main.go
	web/
		static/
	pkg/
		...
	internal/
		...
	build/
		ci/
	package/
	configs/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

这里,cmd/webform目录将包含main包。如你所见,这是一个包名与所在目录名不匹配的例子。Go构建系统将使用目录名创建可执行文件,所以当你构建cmd/webform下的main包时,会得到一个名为webform的可执行文件。如果你在单个模块中构建多个可执行文件,可以在cmd/目录下创建一个与程序名匹配的目录,并在其中创建一个单独的main包。

pkg/目录将包含程序中导出的包。这些包可以被导入并在其他项目中复用。

如果你有在项目外部不可用的包,应该将它们放在internal/目录下。Go构建系统会识别这个目录,并且不允许从internal/目录所在目录之外的其他包中导入internal/目录下的包。通过这种设置,我们webform程序的所有包都可以访问internal/目录下的包,但导入这个模块的其他包无法访问。

web/目录将包含任何与Web相关的资源。在这个例子中,我们会有一个web/static目录,用于存放静态网页。如果你有服务器端模板,还可以添加web/templates目录来存储它们。

build/package目录应该包含用于云、容器、打包系统(如dep、rpm、pkg等)的打包脚本和配置。

build/ci目录应该包含持续集成工具脚本和配置。如果你使用的持续集成工具要求其文件存放在特定目录而非此目录,你可以创建符号链接,或者直接将这些文件放在工具所需的位置,而不是/build/ci目录。

configs/目录应该包含配置文件模板和默认配置。你也会看到一些项目将main包放在模块根目录下,省略了cmd/目录。当模块只有一个可执行文件时,这是一种常见的布局:

webform/
    go.mod
    go.sum
    main.go
    internal/
    	...
    pkg/
    	...
1
2
3
4
5
6
7
8

还有一些模块没有main包。这些通常是可以导入到你的项目中的库。例如,https://github.com/google/uuid (opens new window) 使用扁平目录结构,包含了流行的UUID实现。

# 构建和运行程序

现在你已经有了一个模块以及包含一些Go文件的源代码树,接下来就可以构建或运行程序了。

# 操作方法……

  • 使用go build构建当前包。
  • 使用go build ./path/to/package构建指定目录中的包。
  • 使用go build <moduleName>构建一个模块。
  • 使用go run运行当前的主包(main package)。
  • 使用go run ./path/to/main/package构建并运行指定目录中的主包。
  • 使用go run <moduleName/mainpkg>构建并运行指定目录下模块的主包。

我们来编写启动HTTP服务器的主函数。以下代码片段来自cmd/webform/main.go:

package main

import (
    "net/http"
)

func main() {
    server := http.Server{
        Addr:    ":8181",
        Handler: http.FileServer(http.Dir("web/static")),
    }
    server.ListenAndServe()
}
1
2
3
4
5
6
7
8
9
10
11
12
13

目前,main函数仅导入了标准库中的net/http包。它启动了一个服务器,用于提供web/static目录下的文件服务。请注意,要使该程序正常运行,必须从模块根目录运行:

$ go run ./cmd/webform
1

始终运行主包,避免使用go run main.go。使用go run main.go会运行main.go文件,但不包含主包中的任何其他文件。如果主包中还有包含辅助函数的其他.go文件,这种方式将会失败。

如果你从其他目录运行这个程序,它将无法找到web/static目录,因为这是一个相对路径,会相对于当前目录进行解析。

当你通过go run运行程序时,程序的可执行文件会被放置在一个临时目录中。要构建可执行文件,请使用以下命令:

$ go build ./cmd/webform
1

这会在当前目录中创建一个二进制文件。二进制文件的名称将由主包的最后一段路径决定,在这个例子中是webform。要构建一个不同名称的二进制文件,请使用以下命令:

$ go build -o wform ./cmd/webform
1

这将构建一个名为wform的二进制文件。

# 导入第三方包

大多数项目都会依赖于必须导入的第三方库。Go模块系统(Go module system)用于管理这些依赖项。

# 操作方法……

  1. 找到项目中需要使用的包的导入路径。
  2. 在使用外部包的源文件中添加必要的导入语句。
  3. 使用go get或go mod tidy命令将模块添加到go.mod和go.sum文件中。如果该模块之前未下载过,此步骤还将下载该模块。

提示

你可以使用 https://pkg.go.dev (opens new window) 来查找包。它也是发布你所发布的Go项目文档的地方。

让我们为上一节的程序添加一个数据库,以便存储网页表单提交的数据。在这个练习中,我们将使用SQLite数据库。

修改cmd/webform/main.go文件,导入数据库包并添加必要的数据库初始化代码:

package main

import (
    "net/http"
    "database/sql"
    _ "modernc.org/sqlite"
    "github.com/PacktPublishing/Go-Recipes-for-Developers/src/chp1/webform/pkg/commentdb"
)

func main() {
    db, err := sql.Open("sqlite", "webform.db")
    if err != nil {
        panic(err)
    }
    
    commentdb.InitDB(db)
    server := http.Server{
        Addr:    ":8181",
        Handler: http.FileServer(http.Dir("web/static")),
    }
    
    server.ListenAndServe()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

_ "modernc.org/sqlite"这一行将SQLite驱动程序导入到项目中。下划线是空白标识符,这意味着sqlite包不会被此文件直接使用,导入它只是为了其副作用。如果没有这个空白标识符,编译器会抱怨导入未被使用。在这个例子中,modernc.org/sqlite包是一个数据库驱动程序,当你导入它时,其init()函数会向标准库注册所需的驱动程序。

下一个声明从我们的模块中导入commentdb包。请注意,导入包时使用了完整的模块名称。构建系统会将这个导入声明的前缀识别为当前模块名称,并将其转换为本地文件系统引用,在这个例子中是webform/pkg/commentdb。

在db, err := sql.Open("sqlite", "webform.db")这一行,我们使用database/sql包中的Open函数启动一个SQLite数据库实例。sqlite指定了数据库驱动程序,它是由导入的_ "modernc.org/sqlite"注册的。

commentdb.InitDB(db)语句将调用commentdb包中的一个函数。

现在,让我们看看commentdb.InitDB函数的内容。这是webform/pkg/commentdb/initdb.go文件:

package commentdb

import (
    "context"
    "database/sql"
)

const createStmt=`create table if not exists comments (
    email TEXT,
    comment TEXT)`

func InitDB(conn *sql.DB) {
    _, err := conn.ExecContext(context.Background(), createStmt)
    if err != nil {
        panic(err)
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

如你所见,这个函数会在数据库表尚未创建时创建它们。

请注意InitDB的大小写形式。如果一个包中声明的符号名称的首字母是大写字母,那么这个符号可以从其他包中访问(即它是导出的)。如果不是大写字母,该符号只能在声明它的包内使用(即它未被导出)。createStmt常量未被导出,对其他包来说是不可见的。

让我们构建这个程序:

$ go build ./cmd/webform
cmd/webform/main.go:7:2: no required module provides package
modernc.org/sqlite; to add it:
go get modernc.org/sqlite
1
2
3
4

你可以运行go get modernc.org/sqlite将该模块添加到项目中。或者,你也可以运行以下命令:

$ go get
1

这将获取所有缺失的模块。另外,你还可以运行以下命令:

$ go mod tidy
1

go mod tidy会下载所有缺失的包,使用更新后的依赖项更新go.mod和go.sum文件,并删除对任何未使用模块的引用。go get只会下载缺失的模块。

# 导入特定版本的包

有时,由于API不兼容或依赖特定行为,你需要使用第三方包的特定版本。

# 操作方法……

  • 要获取包的特定版本,请指定版本标签:
$ go get modernc.org/sqlite@v1.26.0
1
  • 要获取包的特定主版本的最新发布版本,可使用以下命令:
$ go get gopkg.in/yaml.v3
1

或者,使用这个命令:

$ go get github.com/ory/dockertest/v3
1
  • 要导入最新可用版本,可使用以下命令:
$ go get modernc.org/sqlite
1
  • 你还可以指定不同的分支。如果存在devel分支,以下命令将从该分支获取模块:
$ go get modernc.org/sqlite@devel
1
  • 或者,你可以获取特定的提交版本:
$ go get modernc.org/sqlite@a8c3eea199bc8fdc39391d5d261eaa3577566050
1

如你所见,你可以使用@revision约定获取模块的特定修订版本:

$ go get modernc.org/sqlite@v1.26.0
1

URL中的修订部分由版本控制系统(在这个例子中是Git)进行评估,所以任何有效的Git修订语法都可以使用。

提示:

你可以查看Go安装目录下的src/cmd/go/alldocs.go文件,了解支持哪些版本控制系统。

这也意味着你可以使用分支:

$ go get modernc.org/sqlite@master
1

提示 https://gopkg.in (opens new window) 服务会将版本号转换为与Go构建系统兼容的URL。有关如何使用它的说明,请参考该网站上的内容。

# 使用模块缓存

模块缓存(module cache)是Go构建系统存储下载的模块文件的目录。本节将介绍如何使用模块缓存。

# 操作方法……

模块缓存默认位于$GOPATH/pkg/mod目录下,当未设置GOPATH时,其路径为$HOME/go/pkg/mod:

  • 默认情况下,Go构建系统在模块缓存中创建只读文件,以防止意外修改。
  • 要验证模块缓存未被修改且反映了模块的原始版本,可使用以下命令:
go mod verify
1
  • 要清理模块缓存,可使用以下命令:
go clean -modcache
1

有关模块缓存的权威信息来源是Go Modules Reference(https://go.dev/ref/mod (opens new window))。

# 使用内部包减少API暴露面

并非每一段代码都是可复用的。较小的API暴露面(API surface)能让其他人更轻松地适配和使用你的代码。因此,你不应导出特定于你程序的API。

# 操作方法……

创建内部包(internal package)以向其他包隐藏实现细节。内部包下的任何内容只能从包含该内部包的包下的其他包中导入,也就是说,myproject/internal下的任何内容只能从myproject下的包中导入。

在我们的示例中,我们将数据库访问代码放在了一个包中,其他程序可以访问该包。然而,将HTTP路由暴露给其他人是没有意义的,因为它们特定于这个程序。所以,我们将把它们放在webform/internal包下。

这是internal/routes/routes.go文件:

package routes

import (
    "database/sql"
    "github.com/gorilla/mux"
    "net/http"
)

func Build(router *mux.Router, conn *sql.DB) {
    router.Path("/form").
        Methods("GET").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            http.ServeFile(w, r, "web/static/form.html")
        })
    router.Path("/form").
        Methods("POST").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            handlePost(conn, w, r)
        })
}

func handlePost(conn *sql.DB, w http.ResponseWriter, r *http.Request) {
    email := r.PostFormValue("email")
    comment := r.PostFormValue("comment")
    _, err := conn.ExecContext(r.Context(), "insert into comments (email,comment) values (?,?)",
        email, comment)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    
    http.Redirect(w, r, "/form", http.StatusFound)
}
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
29
30
31

然后,我们修改main.go文件以使用内部包:

package main

import (
    "database/sql"
    "net/http"
    "github.com/gorilla/mux"
    _ "modernc.org/sqlite"
    "github.com/PacktPublishing/Go-Recipes-for-Developers/src/chp1/webform/internal/routes"
    "github.com/PacktPublishing/Go-Recipes-for-Developers/src/chp1/webform/pkg/commentdb"
)

func main() {
    db, err := sql.Open("sqlite", "webform.db")
    if err != nil {
        panic(err)
    }
    
    commentdb.InitDB(db)
    r := mux.NewRouter()
    routes.Build(r, db)
    server := http.Server{
        Addr:    ":8181",
        Handler: r,
    }
    server.ListenAndServe()
}
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

# 使用模块的本地副本

有时,你会同时处理多个模块,或者从代码仓库下载一个模块,对其进行一些修改,然后希望使用修改后的版本,而不是代码仓库中的版本。

# 操作方法……

在go.mod文件中使用replace指令,指向包含模块的本地目录。

让我们回到之前的示例——假设你想对sqlite包进行一些修改:

  1. 克隆它:
$ ls

	webform

$ git clone git@gitlab.com:cznic/sqlite.git

$ ls

    sqlite
    webform
1
2
3
4
5
6
7
8
9
10
  1. 修改项目下的go.mod文件,指向模块的本地副本。go.mod文件内容变为如下:
module github.com/PacktPublishing/Go-Recipes-for-Developers/chapter1/webform

go 1.22.1

replace modernc.org/sqlite => ../sqlite

require (
    github.com/gorilla/mux v1.8.1
    modernc.org/sqlite v1.27.0
)
1
2
3
4
5
6
7
8
9
10
  1. 现在,你可以在系统上的sqlite模块中进行修改,这些修改将被构建到你的应用程序中。

# 处理多个模块——工作区(workspace)

有时,你需要处理多个相互依赖的模块。一种便捷的方法是定义一个工作区。工作区就是一组模块。如果工作区内的一个模块引用了同一工作区中另一个模块的包,它将在本地进行解析,而不是通过网络下载该模块。

# 操作方法……

  1. 要创建一个工作区,你必须有一个包含所有工作模块的父目录:
$ cd ~/projects
$ mkdir ws
$ cd ws
1
2
3
  1. 然后,使用以下命令启动一个工作区:
$ go work init
1

这将在该目录中创建一个go.work文件。 3. 将你正在处理的模块放入这个目录。

让我们用示例来演示。假设我们有以下目录结构:

$HOME/
    projects/
    	ws/
    		go.work
    		webform
    		sqlite
1
2
3
4
5
6

现在,我们想将webform和sqlite这两个模块添加到工作区。为此,使用以下命令:

$ go work use ./webform
$ go work use ./sqlite
1
2

这些命令会将两个模块添加到你的工作区。现在,webform模块中对sqlite的任何引用都将解析为使用该模块的本地副本。

# 管理模块版本

Go工具使用语义化版本控制系统。这意味着版本号采用X.Y.z的形式,具体分解如下:

  • X:在进行重大版本发布时增加,重大版本发布不一定向后兼容。
  • Y:在进行次要版本发布时增加,次要版本发布是增量式的,且向后兼容。
  • z:在进行向后兼容的补丁(patch)发布时增加。

你可以在https://semver.org (opens new window)上了解更多关于语义化版本控制的信息。

# 操作方法……

  • 要发布补丁版本或次要版本,使用新的版本号标记包含你修改的分支:
$ git tag v1.0.0
$ git push origin v1.0.0
1
2
  • 如果你想发布一个与之前版本的API不兼容的新版本,你应该增加该模块的主版本号。要发布模块的新主版本,使用一个新分支:
$ git checkout -b v2
1

然后,在go.mod文件中更改模块名称,使其以/v2结尾,并更新源文件树中的所有引用,以使用模块的/v2版本。

例如,假设你发布了webform模块的第一个版本v1.0.0。然后,你决定添加新的API端点。这不是一个破坏性的更改,所以你只需增加次要版本号——v1.1.0。但后来发现你添加的一些API导致了问题,所以你删除了它们。现在,这是一个破坏性的更改,所以你应该发布v2.0.0版本。你该怎么做呢?

答案是,在版本控制系统中使用一个新分支。创建v2分支:

$ git checkout -b v2
1

然后,更改go.mod文件以反映新版本:

module github.com/PacktPublishing/Go-Recipes-for-Developers/chapter1/webform/v2

go 1.22.1

require (
    ...
)
1
2
3
4
5
6
7

如果模块中有多个包,你必须更新源文件树,以便模块内对包的任何引用也使用v2版本。

提交并推送新分支:

$ git add go.mod
$ git commit -m "New version"
$ git push origin v2
1
2
3

要使用新版本,你现在必须导入包的v2版本:

import "github.com/PacktPublishing/Go-Recipes-for-Developers/chapter1/webform/v2/pkg/commentdb"
1

# 总结与延伸阅读

本章重点介绍了设置和管理Go项目的概念及操作方法。这绝不是一份详尽的参考资料,但这里介绍的方法应该能让你掌握有效使用Go构建系统的基础知识。

Go模块的权威指南是Go Modules Reference(https://go.dev/ref/mod (opens new window))。查看Managing dependencies链接(https://go.dev/doc/modules/managing-dependencies (opens new window)),可获取关于依赖管理的详细讨论。

在下一章中,我们将开始处理文本数据。

Go开发实用指南 说明
第2章 字符串处理

← Go开发实用指南 说明 第2章 字符串处理→

最近更新
01
第二章 关键字static及其不同用法
03-27
02
第一章 auto与类型推导
03-27
03
第四章 Lambda函数
03-27
更多文章>
Copyright © 2024-2025 沪ICP备2023015129号 张小方 版权所有
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式