背景

这是我学习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 目录或 pipvirtualenv 机制。
  • Makefile 包含了项目的构建与管理规则,如编译、测试、部署等。
  • README.md 文件包含了项目的说明文档和使用说明。

这种目录布局其实还挺清晰的,但是自从go引入go modules做依赖管理,项目布局结构会变得更精简,更灵活。具体目录布局如下所示:

.
├── cmd
│ └── main.go
├── internal
├── pkg
├── vendor
├── go.mod
└── README.md
  • cmd 目录是程序的入口代码,即 main 函数的实现。
  • internal 目录用于存放应用程序的私有代码,不能被其他项目引用。
  • pkg 目录用于存放应用程序的公共库代码,可以被其他项目引用。
  • vendor 目录用于存放依赖库的代码,类似于其他语言中的 node_modules 目录或 pipvirtualenv 机制。
  • 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.goadmin.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.TErrorFailFatal 函数,以及 testing.BReportAllocsResetTimerStopTimer 等函数。

下面是一个简单的测试示例,测试 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时遇到的似懂非懂的概念的更多相关文章

  1. 浅谈学习C++时用到的【封装继承多态】三个概念

    封装继承多态这三个概念不是C++特有的,而是所有OOP具有的特性. 由于C++语言支持这三个特性,所以学习C++时不可避免的要理解这些概念. 而在大部分C++教材中这些概念是作为铺垫,接下来就花大部分 ...

  2. 学习Golang语言(6):类型--切片

    学习Golang语言(1): Hello World 学习Golang语言(2): 变量 学习Golang语言(3):类型--布尔型和数值类型 学习Golang语言(4):类型--字符串 学习Gola ...

  3. 上四条只是我目前总结菜鸟们在学习FPGA时所最容易跑偏的地

    长期以来很多新入群的菜鸟们总 是在重复的问一些非常简单但是又让新手困惑不解的问题.作为管理员经常要给这些菜鸟们普及基础知识,但是非常不幸的是很多菜鸟怀着一种浮躁的心态来学习 FPGA,总是急于求成. ...

  4. 2.golang应用目录结构和GOPATH概念

    golang 语言有一个GOPATH的概念就是当前工作目录 [root@localhost golang_test]# tree . ├── bin │   └── hello ├── first.g ...

  5. 在学习泛型时遇到的困惑经常与func<T,U>混淆

    在学习泛型时遇到的困惑经常与func<T,U>混淆,总认为最后一个值是返回类型.现在区分一下,原来问题出在泛型委托上. C#委托的介绍(delegate.Action.Func.predi ...

  6. 学习 C++,关键是要理解概念,而不应过于深究语言的技术细节

    学习 C++学习 C++,关键是要理解概念,而不应过于深究语言的技术细节. 学习程序设计语言的目的是为了成为一个更好的程序员,也就是说,是为了能更有效率地设计和实现新系统,以及维护旧系统. C++ 支 ...

  7. ASP.NET Core on K8S学习初探(2)K8S基本概念快速一览

    在上一篇<单节点环境搭建>中,通过Docker for Windows在Windows开发机中搭建了一个单节点的K8S环境,接下来就是动人心弦的部署ASP.NET Core API到K8S ...

  8. 学习servlet时出现的一些问题

    此篇用来记录学习servlet时遇到的一些问题,谨防以后再犯. 问题1.导入的web项目,servlet中导入的包名报错. (1)缺少相关包,推荐一个网站下载jar包很方便http://mvnrepo ...

  9. JVM学习-运行时数据区域

    目录 前言 运行时数据区 程序计数器 Java虚拟机栈 局部变量表 基础数据类型 对象引用 returnAddress 操作数栈 动态链接 方法返回地址 Java堆 方法区 类型信息 字段描述符 方法 ...

  10. 前端程序员学习 Golang gin 框架实战笔记之一开始玩 gin

    原文链接 我是一名五六年经验的前端程序员,现在准备学习一下 Golang 的后端框架 gin. 以下是我的学习实战经验,记录下来,供大家参考. https://github.com/gin-gonic ...

随机推荐

  1. weblogic11g打补丁,应用出现乱码

    解决办法: 1.找到域下的这个路径:autodeploy\manager\WEB-INF里的web.xml文件,先备份好,再添加以下语句: <context-param> <para ...

  2. ubuntu下删除U盘文件到回收站无法清空问题的解决

    Ubuntu可以自动加载U盘 每当,拷贝新的文件,而空间不足的时候,就会删除原有的文件. 可是,它不是彻底删除,而是放在垃圾箱中(/home/mrc/.local/share/Trash/files) ...

  3. 【Leetcode】 剑指offer:链表(简单)--Day02

    剑指Offer 06. 从尾到头打印链表 可借助栈. 或先遍历列表得到元素数,开辟数组空间倒序填入. 剑指 Offer 24. 反转链表 可借助栈: class Solution { public L ...

  4. react hooks(useState、useEffect、useRef详解)

    好巧不巧,工作了一年跳槽了,之前用的vue,现在用的react- 嗯!工作使人进步!现在开始学react吧! 切入正题- react hooks是React16.8.0之后出现的, 类组件存在的问题: ...

  5. 关于Go语言的底层,你想知道的都在这里!

    目录 1. GoLang语言 1.1 Slice 1.2 Map 1.3 Channel 1.4 Goroutine 1.5 GMP调度 1.6 垃圾回收机制 1.7 其他知识点 2. Web框架Gi ...

  6. 重构SeleniumeDownloader底层浏览器驱动

    一.解决bug:Selenium with PhantomJS,重构SeleniumeDownloader底层浏览器驱动 0.小背景: 想爬取外网steam的数据,但是steam官网在海外,加上ste ...

  7. Hadoop-HA节点介绍

    设计思想 hadoop2.x启用了主备节点切换模式(1主1备) 当主节点出现异常的时候,集群直接将备用节点切换成主节点 要求备用节点马上就要工作 主备节点内存几乎同步 有独立的线程对主备节点进行监控健 ...

  8. Redis Stream Commands 命令学习-1 XADD XRANGE XREVRANGE

    概况 A Redis stream is a data structure that acts like an append-only log. You can use streams to reco ...

  9. Typora编辑区域空白过大问题

    参考博客:https://blog.csdn.net/m0_55485287/article/details/115207178 在哪个文件编辑? 1.找到使用的主题,打开主题文件夹 2.找到对应的c ...

  10. requests发送post请求

    post请求 语法结构 requests.post(url,data = None,json = None) 参数说明 url:需要爬取的网站的网址 data:请求数据 json:json格式的数据 ...