学习Golang时遇到的似懂非懂的概念

这篇具有很好参考价值的文章主要介绍了学习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 目录或 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:图有点糊,将就一下......

学习Golang时遇到的似懂非懂的概念

学习Golang时遇到的似懂非懂的概念

项目结构目录重点突出一个清晰明了,我们需要清楚每个目录代表的含义是什么,每个目录下的文件有哪些,目录文件的调用关系,目录文件的隐蔽性等等。

为什么一定是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/

写的这么详细,诸位点个赞不过分吧!!!!文章来源地址https://www.toymoban.com/news/detail-436425.html

到了这里,关于学习Golang时遇到的似懂非懂的概念的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • golang学习-golang结构体和Json相互转换

    1、结构体转为json对象     v, _ := json.Marshal(student)     jsonStr := string(v) // 结构体转为json对象 2、json字符串转为结构体     var s1 Student     err := json.Unmarshal([]byte(str), s1) //json 字符串转为结构体    3、结构体标签 表示的是转换为json对象时,ID字段变为id,Name字段变为name. type

    2024年01月23日
    浏览(55)
  • golang学习-函数

    1、匿名函数 没有函数名的函数,格式如下: func(参数)返回值{ 函数体 } 2、闭包 全局变量的特点:常驻内存、污染全局 局部变量的特点:不常驻内存、不污染全局 闭包:可以让一个变量常驻内存并且不污染全局 概念:可以访问另一个函数作用域中的变量的函数 注意:由于

    2024年02月02日
    浏览(42)
  • 开启golang学习之路

    Go 编程语言是一个开源项目,旨在提高程序员的工作效率。 Go 富有表现力、简洁、干净且高效。它的并发机制使编写能够充分利用多核和联网机器的程序变得容易,而其新颖的类型系统可以实现灵活和模块化的程序构建。 Go 可以快速编译为机器代码,同时还具有垃圾收集的

    2024年01月25日
    浏览(49)
  • golang学习-结构体

    1、定义 使用type 和struct 来定义结构体,是值类型 格式如下: type 类型名 struct {         字段名 类型         字段名 类型         ... } 2、实例化 1、var 结构体实例 结构体类型    var p1 Person   2、使用new   var p2 = new(Person) 3、使用对结构体进行取地址操作

    2024年01月22日
    浏览(38)
  • golang学习-指针

    1、定义 指针也是一个变量,但它是一个特殊的变量,它存储的是另一个变量的内存地址。是引用数据类型。 取一个变量的地址:a  定义: var p *int = a 可以理解为 指针变量p中存储的是a的内存地址,但是变量p也有自己的内存地址。 2、指针取值 *p 表示的是 取出p这个变量对

    2024年02月02日
    浏览(36)
  • Golang爬虫学习

    2023年将会持续于B站、CSDN等各大平台更新,可加入粉丝群与博主交流:838681355,为了老板大G共同努力。 【商务合作请私信或进群联系群主】 1.1 简介和示例 1.2 爬虫解析页面 1.2.1 解析链接页面 1.2.2 解析内容页面 1.2.3 代码实例 1.3 保存至文件 1.4 保存至数据库 2.1 爬取示例 2.2

    2024年02月11日
    浏览(42)
  • golang学习-channel管道

    1、定义 管道是golang语言提供的goroutine间的通讯方式,channel可以让一个goroutine发送特定的值给另一个goroutine的通讯机制。 管道是引用类型。 golang语言中channel是一种特殊的类型。像一个队列一样,先进先出。 var 变量 chan 元素类型 var ch1 chan int //声明一个传递整型的管道 var

    2024年01月19日
    浏览(50)
  • Golang学习+深入(十一)-文件

    目录 一、概述 1、文件 1.1、打开文件和关闭文件 1.2、读文件 1.3、写文件 1.4、判断文件是否存在 1.5、拷贝文件 文件: 文件是数据源(保存数据的地方) 的一种,比如word文档,txt文件,excel文件...都是文件。文件最主要的作用就是保存数据,它既可以保存一张图片,也可以保存

    2023年04月14日
    浏览(60)
  • golang学习-goroutine

    1、goroutine协程 goroutine 是 Go 语言支持并发的核心,一个goroutine会以一个很小的栈开始其生命周期,一般只需要2KB。区别于操作系统线程由系统内核进行调度, goroutine 是由Go运行时(runtime)负责调度。例如Go运行时会智能地将 m个goroutine 合理地分配给n个操作系统线程,实现类

    2024年01月18日
    浏览(42)
  • TypeScript学习笔记以及学习中遇到的问题

    本笔记是来自翻阅xcatliu的typeScript入门教程文档、TypeScript官方文档的部分摘录、以及观看B站学习视频进行笔记记录与知识点补充、本人实际使用时遇到的问题与解决记录、碎片化接触到相关知识点合并整理而成 TypeScript 的命令行工具安装方法如下: 以上命令会在全局环境下

    2023年04月24日
    浏览(53)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包