学习Golang时遇到的似懂非懂的概念
背景
这是我学习golang的第三天,大致已经掌握了golang的语法,但是着手开发的时候,却遇到了许多问题,例如golang导包机制、golang的项目管理规范、go mod生成project怎么管理依赖的等等。其实这些概念之前也了解过,但是也只是如蜻蜓点水般的了解。正好,现在遇到了这些问题,那便认真总结一番。
问题总结
一个优秀的Go项目的布局是怎样的?
这个我在网上搜了很多的资料,不管是博客还是视频,他们大部分教的是在Go ENV路径下创建你的project,然后cd到你的project,接着在该项目文件夹下创建bin、src 和pkg目录。目录布局大致如下:
.
├── bin
├── pkg
├── src
│ ├── github.com
│ │ ├── user
│ │ │ └── project1
│ │ └── user
│ │ └── project2
│ ├── main.go
│ └── other.go
├── vendor
├── Makefile
└── README.md
bin目录放置编译生成的可执行文件。pkg目录放置编译生成的包文件。src目录是源码目录,它包含了项目的所有Go源代码,外部依赖库的源代码也可以放在该目录下。vendor目录存储了第三方依赖库的代码,类似于其他语言中的node_modules目录或pip的virtualenv机制。Makefile包含了项目的构建与管理规则,如编译、测试、部署等。README.md文件包含了项目的说明文档和使用说明。
这种目录布局其实还挺清晰的,但是自从go引入go modules做依赖管理,项目布局结构会变得更精简,更灵活。具体目录布局如下所示:
.
├── cmd
│ └── main.go
├── internal
├── pkg
├── vendor
├── go.mod
└── README.md
cmd目录是程序的入口代码,即main函数的实现。internal目录用于存放应用程序的私有代码,不能被其他项目引用。pkg目录用于存放应用程序的公共库代码,可以被其他项目引用。vendor目录用于存放依赖库的代码,类似于其他语言中的node_modules目录或pip的virtualenv机制。go.mod是 Go modules 的配置文件,用于管理依赖关系和版本控制。README.md文件包含了项目的说明文档和使用说明。
当然根据业务要求,我们还可以添加docs目录存放项目文档,添加test目录存放单元测试代码。
Tonybai大佬的图画的相当不错,我这边引用一下,大家看完图就知道一个Go语言的经典布局该是什么样的了。
ps:图有点糊,将就一下......


项目结构目录重点突出一个清晰明了,我们需要清楚每个目录代表的含义是什么,每个目录下的文件有哪些,目录文件的调用关系,目录文件的隐蔽性等等。
为什么一定是go mod?
我提出这个问题并不是吹毛求疵,而是真心想了解在没go module管理时,大家不也写的好好的么,为什么go module出来后,大家会立马抛弃以前的做法。这 go module到底带来了什么好处,如此吸引人。
我还是从项目布局上理解,在没有go module管理时,大家的项目布局应该长这样:
.
├── bin
├── pkg
├── src
│ ├── github.com
│ │ ├── user
│ │ │ └── project1
│ │ └── user
│ │ └── project2
│ ├── main.go
│ └── other.go
├── vendor
├── Makefile
└── README.md
首先在 Go 1.11 版本之前,如果要在 $GOPATH 中运行一个项目,该项目必须存放在 $GOPATH/src 目录下。布局里就包含了project1和project2两个项目文件目录。这会导致你的src目录越来越肿大。
其次是手动管理依赖问题。在Go 1.11版本之前,Go 语言没有官方的依赖管理工具,因此在项目中引入外部依赖库的时候,通常需要手动将依赖库的代码拷贝到 $GOPATH/src 目录下。这个光不是手动下载的事,你还要考虑到手动更新,依赖之间的冲突问题,部署的问题等等。在倡导DEVOPS的时代,哪一个都是让人分心头痛的事。
然而,go module解决了上述问题(怪不得大家会极力拥抱这门新技术)。这里举个例子说明两者的差别,让思路更清晰。
假设有两个项目 A 和项目 B,都依赖于 Go 语言的一个第三方库 github.com/gin-gonic/gin。其中,项目 A 使用传统的依赖管理方式,项目 B 使用 Go modules 来管理依赖。
一、使用传统的依赖管理方式的项目 A:
项目 A 的目录结构如下:
.
├── main.go
└── vendor
└── github.com
└── gin-gonic
└── gin
├── LICENSE
├── README.md
├── bindings
├── contributing.md
├── favicon.ico
├── gin.go
├── go.mod
├── go.sum
├── handlers
├── logger.go
├── middleware.go
├── render
├── router.go
└── vendor
main.go 文件中引入了 github.com/gin-gonic/gin:
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
// ...
}
在项目 A 中,依赖库 github.com/gin-gonic/gin 的代码被存放在 vendor 目录中,无法和其他项目共享使用。需要说明地是,使用 vendor 目录来管理依赖库是 Go 语言在 Go 1.5 版本时引入的方式。在使用 vendor 目录管理依赖库的时候,你需要将依赖库的代码复制到项目目录下的 vendor 目录下,然后在代码中引用这些依赖库。如果这个依赖库的版本发生升级,需要手动更新并重新拷贝代码到 vendor 目录,容易出现版本冲突或遗漏问题。
二、使用 Go modules 的项目 B:
项目 B 的目录结构如下:
.
├── go.mod
├── main.go
└── vendor
main.go 文件中同样引入了 github.com/gin-gonic/gin:
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
// ...
}
在项目 B 中,使用 go mod 命令来管理依赖库和版本,无需手动拷贝依赖库代码。执行以下命令会自动下载依赖库代码至 $GOPATH/pkg/mod 目录下:
go mod init example.com/B
go mod tidy
安装完依赖库后,可以把 $GOPATH/pkg/mod 目录下的 github.com/gin-gonic/gin 目录拷贝到其他项目中使用,从而实现依赖共享。如果需要升级或切换依赖库的版本,只需要修改 go.mod 文件中的版本号即可,不会影响到其他项目。
这里可能有一个歧义的点,就是“拷贝到其它项目中使用”这个说法。依我看,go mod会自动安装你所需要库的依赖,它会在本地留下缓存信息。那么如果另一个项目中也需要使用 github.com/gin-gonic/gin,可以直接在代码中引用该依赖库,如下所示:
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
// ...
}
只要之前已经在任意一个项目中使用了 go mod 命令下载过并缓存了 github.com/gin-gonic/gin,Go 会自动从缓存中加载依赖库代码,而不会重新下载依赖库代码到 $GOPATH/pkg/mod 目录下。
这种方式可以实现依赖库的共享,避免了多个项目同时拷贝依赖库代码,节省了磁盘空间。同时,如果需要使用不同的版本号,可以通过修改 go.mod 文件来实现对特定版本的依赖管理。
当你修改了 go.mod 文件中的依赖版本号或者添加了新的依赖项之后,可以使用 go mod tidy 命令来更新依赖关系,例如:
# 修改 go.mod 文件中的依赖版本号
go mod edit -require github.com/gin-gonic/gin@v1.7.2
# 更新依赖关系
go mod tidy
go mod tidy 会根据 go.mod 文件中的依赖关系自动下载并更新依赖库代码,以保持依赖关系的一致性,并且会删除未被引用的依赖项。
另外,如果你还希望移除某一个不再使用的依赖库,可以使用 go mod tidy -v 命令,它会输出垃圾收集的详细信息,包括哪些依赖项是被移除的。
Go的导包机制是什么?️
我的人生信条就是优雅,如何优雅地导包也是我所追求的。
在了解怎么导包之前,我们先需要了解包在Go里面的具象化的体现是什么?包和模块的区别是什么?
抛开语言,从软件角度考虑,我们认为子程序的集合称为包,包的集合称为模块,模块的集合称为子系统,子系统的集合称为系统。将这个说法往Go语言上代入,能发现我们编写的go文件就是子程序,go文件所在文件夹就是包,根目录的文件名就是模块名。这些具象化的体现还能从哪里看出来呢?其实还能从我们创建的文件中看出。
以这样的目录结构为例:
project/
|- go.mod
|- main.go
|- controllers/
|- user.go
|- admin.go
其中:
go.mod是模块文件,位于项目根目录。main.go是入口文件,位于项目根目录。controllers/目录下有两个文件user.go和admin.go
一般来说,当你查看go.mod文件时,能看到第一行写着
module project
这是告诉你模块名是project。如果以后你的代码开源了,别人想引用你的代码,首先就是要根据你的模块名再找对应的包名。这和其它语言引包的方式很相似。
module后面的模块名是允许修改的,你可以换成自定义的写法,通常的写法是域名+项目名。
再打开user.go,你能发现第一行写着
package controllers
这是指user.go是controllers包下面的一个子程序。
理清楚这些概念,我们还需要记住一个总纲:“导包其实就是寻址!导包其实就是寻址!导包其实就是寻址!”。重要的话说三遍!
还是以上面的目录结构为例,如果需要在 main.go 中导入 controllers/user.go 文件中的代码,可以使用相对路径 ./controllers 来导入 user.go 文件。
// main.go
package main
import "./controllers"
func main() {
// ...
}
对于模块,可以使用模块路径来引用相对路径下的文件。例如,假设你的模块路径为 example.com/mymodule,则可以在 main.go 中使用 example.com/mymodule/controllers 来引用相对路径下的 user.go 文件。
// main.go
package main
import "example.com/mymodule/controllers"
func main() {
// ...
}
需要注意的是,使用相对路径导入包时,包的实际路径是相对于当前文件所在的目录。如果在其他文件中也要使用相对路径导入 controllers/user.go,则需要将路径设置为相对于这个文件的路径。同时,相对路径只适用于模块内的代码,如果要在不同的模块之间导入代码,必须使用完整的包路径。
Go的测试代码怎么生成?
在 Go 中,我们可以通过创建测试文件来编写测试代码。测试文件应该遵循 Go 的命名规则,即在文件名后面加上 _test。例如,如果要编写一个名为 sum 的函数的测试代码,那么测试文件的文件名应该是 sum_test.go,而被测试的函数前面要加个Test前缀。
在测试文件中,我们可以使用 testing 包提供的一系列函数来编写测试代码,例如 testing.T 的 Error、Fail 和 Fatal 函数,以及 testing.B 的 ReportAllocs、ResetTimer 和 StopTimer 等函数。
下面是一个简单的测试示例,测试 sum 函数:
// sum_test.go
package main
import "testing"
func TestSum(t *testing.T) {
tables := []struct {
a, b, expected int
}{
{1, 1, 2},
{2, 2, 4},
{3, 3, 6},
}
for _, table := range tables {
result := sum(table.a, table.b)
if result != table.expected {
t.Errorf("Sum of %d + %d was incorrect, expected %d but received %d", table.a, table.b, table.expected, result)
}
}
}
在这个测试文件中,我们首先导入 testing 包,并定义了一个名为 TestSum 的测试函数。tables 定义了一个结构体切片,其中包含了一组要测试的参数和期望结果。我们遍历 tables 切片,逐一测试每组数据,判断计算结果是否和期望的结果一致。如果不一致,我们使用 t.Errorf 函数输出错误信息。这样就完成了一个简单的测试文件。
要运行测试文件,可以在项目根目录下使用 go test 命令运行。Go 会自动查找并运行所有的测试文件,并输出测试结果。测试结果中会显示测试用例的数量、测试是否通过以及每个测试用例的具体信息。例如:
$ go test
PASS
ok project 0.008s
在本示例中,我们只有一个测试用例,测试结果判定为 “ PASS ”,表示测试通过了。如果测试失败,则测试结果会判定为 “ FAIL ”,并输出具体的错误信息。
参考资料
https://tonybai.com/2022/04/28/the-standard-layout-of-go-project/
写的这么详细,诸位点个赞不过分吧!!!!
学习Golang时遇到的似懂非懂的概念的更多相关文章
- 浅谈学习C++时用到的【封装继承多态】三个概念
封装继承多态这三个概念不是C++特有的,而是所有OOP具有的特性. 由于C++语言支持这三个特性,所以学习C++时不可避免的要理解这些概念. 而在大部分C++教材中这些概念是作为铺垫,接下来就花大部分 ...
- 学习Golang语言(6):类型--切片
学习Golang语言(1): Hello World 学习Golang语言(2): 变量 学习Golang语言(3):类型--布尔型和数值类型 学习Golang语言(4):类型--字符串 学习Gola ...
- 上四条只是我目前总结菜鸟们在学习FPGA时所最容易跑偏的地
长期以来很多新入群的菜鸟们总 是在重复的问一些非常简单但是又让新手困惑不解的问题.作为管理员经常要给这些菜鸟们普及基础知识,但是非常不幸的是很多菜鸟怀着一种浮躁的心态来学习 FPGA,总是急于求成. ...
- 2.golang应用目录结构和GOPATH概念
golang 语言有一个GOPATH的概念就是当前工作目录 [root@localhost golang_test]# tree . ├── bin │ └── hello ├── first.g ...
- 在学习泛型时遇到的困惑经常与func<T,U>混淆
在学习泛型时遇到的困惑经常与func<T,U>混淆,总认为最后一个值是返回类型.现在区分一下,原来问题出在泛型委托上. C#委托的介绍(delegate.Action.Func.predi ...
- 学习 C++,关键是要理解概念,而不应过于深究语言的技术细节
学习 C++学习 C++,关键是要理解概念,而不应过于深究语言的技术细节. 学习程序设计语言的目的是为了成为一个更好的程序员,也就是说,是为了能更有效率地设计和实现新系统,以及维护旧系统. C++ 支 ...
- ASP.NET Core on K8S学习初探(2)K8S基本概念快速一览
在上一篇<单节点环境搭建>中,通过Docker for Windows在Windows开发机中搭建了一个单节点的K8S环境,接下来就是动人心弦的部署ASP.NET Core API到K8S ...
- 学习servlet时出现的一些问题
此篇用来记录学习servlet时遇到的一些问题,谨防以后再犯. 问题1.导入的web项目,servlet中导入的包名报错. (1)缺少相关包,推荐一个网站下载jar包很方便http://mvnrepo ...
- JVM学习-运行时数据区域
目录 前言 运行时数据区 程序计数器 Java虚拟机栈 局部变量表 基础数据类型 对象引用 returnAddress 操作数栈 动态链接 方法返回地址 Java堆 方法区 类型信息 字段描述符 方法 ...
- 前端程序员学习 Golang gin 框架实战笔记之一开始玩 gin
原文链接 我是一名五六年经验的前端程序员,现在准备学习一下 Golang 的后端框架 gin. 以下是我的学习实战经验,记录下来,供大家参考. https://github.com/gin-gonic ...
随机推荐
- 【LuckyFrame研究】下载安装包
下载安装包,可以快速部署 百度网盘链接: https://pan.baidu.com/s/1SNUhzoKFgH2TRQN2Rx711A 提取码: u845 服务端压缩包 V3.X版本:Lucky ...
- 什么是5G垂直行业?
什么是垂直行业呢? 感觉"垂直行业"这个词在太多地方遇到,但是这个词的涵盖范围到底是什么呢? 垂直这一概念源于两条直线(或平面)的直角交叉,两条直线是相互作为参照物的.比如,我们可 ...
- DVWA-XSS (DOM) DOM型跨站脚本攻击
XSS(Cross Site Scripting),跨站脚本攻击,能使攻击者在页面嵌入一些脚本代码,用户再访问,被诱导点击时,执行恶意脚本,常见为javascript,也有Flash.VBscript ...
- Linux & 标准C语言学习 <DAY3>
C语言简介: BCPL->New B->C->UNIX->Minix->Linux->gcc C语言诞生于1971~1973年,美国贝尔实验室,肯. ...
- 为自己的博客添加2D虚拟人物
2020-05-29 在自己申请完并获得了属于自己的博客后,我突然想着为自己的博客添砖加瓦,记起了之前看别人博客时,其充满独具个性,特立独行的风格,让我十分羡慕,最近看到了一个博客风格很有趣,其有趣之 ...
- 在 Rainbond 上使用 Curve 云原生存储
Curve 是网易主导自研的现代化存储系统, 目前支持文件存储(CurveFS)和块存储(CurveBS). CurveBS 的核心应用场景主要包括: 虚拟机/容器的性能型.混合型.容量型云盘或持久化 ...
- CTF-RE-学习记录-汇编
八进制运算 加法表 1+1=2 1+2=3 2+2=4 1+3=4 2+3=5 3+3=6 1+4=5 2+4=6 3+4=7 4+4=10 1+5=6 2+5=7 3+5=8 4+5=11 5+5= ...
- MQ(消息队列)常见问题梳理
MQ 中 broker 的作用,有无broker有什么差异? MQ(Message Queue)中的broker是消息队列的核心组件之一,它的作用是接收.存储.分发和传递消息.具体来说,broker主 ...
- 关于Java基础中的异常处理知识点
Java中的异常(Exception),史上最全的教程来啦~_smilehappiness的博客-CSDN博客 以及Java:详解Java中的异常(Error与Exception)_王小二(海阔天空) ...
- 二进制安装Kubernetes(k8s) v1.24.0 IPv4/IPv6双栈 (三主俩从)
二进制安装Kubernetes(k8s) v1.24.0 IPv4/IPv6双栈 (三主俩从) Kubernetes 开源不易,帮忙点个star,谢谢了 介绍 kubernetes二进制安装 后续尽可 ...