GO代码组织形式与结构

GO代码组织形式与结构

一 包介绍

​ 我们用go语言开发的程序文件称之为源码文件(源码文件必须以.go结尾)。很明显将程序所有的代码都放入一个源码文件是不合理的,需要分文件管理相关代码,但是随着程序文件的增多,也必须有一种将文件加以组织管理的方式/形式,于是Go引入了”包“的概念。

​ 包是go语言提供的一种虚拟的概念,包声明一致的多个源码文件在逻辑上被组织到一起、同属于一个包

(1)包的声明、包的导入路径、注意的问题

//一:包的声明
// 1、每个源码文件都必须在文件头处声明自己归属的包。
package 包名 // 包名一般总是用小写字母

// 2、包名任意,包名一致的属于同一个包

// 3、包是编译和归档Go程序的最基本单位,一个包中的多个源码文件是一个不可分割的整体

//二:包的导入路径
强调强调强调!!!!!!
包是一个把多个源码文件归一到一起管理的虚拟单位,一定要记住,它只是一个虚拟的概念而已,而实实在在地讲,多个源码文件是要放置到一个实实在在的文件夹下的,这个实实在在的文件夹所处的路径是包的导入路径。包的导入路径很重要,他是包的"家庭住址",是用来找到包的(用在import语句中,稍后介绍),但它绝不等同于包的概念

//三:注意的问题
1、一个文件夹下只能放置一个包,也就是所一个文件夹下放置的多个源码文件的包声明必须一致,go以此来确保一个路径就唯一定位到唯一的一个包。
2、包虽然与文件夹路径是截然不同的意思,但是为了方便记忆,包通常应该声明为文件夹的名字
例如文件夹路径/a/b/c/mypkg,包名应声明为package mypkg,mypkg就为包名

(2)包的区分与放置位置

​ 包是虚拟的、逻辑层面的概念,但包组织的多个源码文件确实是实实在在的,一定要放置在某一文件夹下.

请注意:为了后续书写简洁,笔者直接将包所组织的多个源码文件的存放位置简称为包的存放位置,请读者务必知晓。

​ main包包含着程序的入口,主要用来运行,无论如何,main包可以被放置于任意文件夹下。

​ 笔者将main包之外的包称之为其他包,具体是指内置包、自定义包、下载的第三方包。有别于main包,其他包主要用来被导入使用,放置位置如下

// 内置包
内置包固定被放置在`$GOROOT/src/`下,与任何模式无关

// 自定义包
在未启用modules模式的情况下,自定义包需要放置在GOPATH指定的任意目录下的src中

// 下载的第三方包
在未启用modules模式的情况下,使用go工具链命令下载的第三方包总是默认被存放到GOPATH的第一个目录的src下 

// 强调一下
在早期的Go环境中,自定义的包与下载的第三方包都是放到了$GOPATH/src下,因为早期Go采用的是和GOPATH模式,而且即便是在GO1.14.2版本中,在我们还未学习如何使用任何新模式前,默认使用的仍是GOPATH模式,该模式下,$GOPATH/src是包导入语句检索包位置的地方详见第五小节

​ ps:

//1、内置包是笔者对标准包的一种爱称
//2、但凡存放于GOPATH工作区中的包,官方都称之为工作区包

(3)包的使用

// 1、一个源码文件中声明的函数、类型、变量和常量等标识符/名字对同一包中的所有其他源码文件都可见,不需要加任何前缀即可引用,因为代码包只是一种组织管理源码文件的形式,同一个包下的多个源码文件本就属于一个整体,事实上我们完全可以将一个包当成一个”大文件“去看,毫无疑问这个”大文件“中声明的标识符/名字不能重名

// 2、包名很关键
名为main的包可简称为”main包“,是程序的入口,go程序运行时会查找main包下的main函数,main函数既没有参数声明也没有结果声明,见下图

名不为main的包可称之为”其他包“,是程序的功能/属性集合,此类包用来被其他包导入使用,为何此时包名仍然很关键呢?因为我们导入时用的是导入路径(指的是包所在的路径,该路径不是绝对路径,稍后介绍),但是使用的则可能会使用"包名.xxx"

​ 综上:包是程序功能/属性”分散管理“ 、”归一使用“的一种逻辑意义上的组织形式

二 main包的使用

1、无论如何,main包可以放置在任意文件夹下

/test/ ==========>main包组织了三个源码文件m3.go、m2.go、m1.go,被放置在了文件夹/test下
├── m3.go
├── m2.go
└── m1.go

m3.go

package main // 声明自己归属于main包

import "fmt" // 导入内置包

func nnn(){
    fmt.Println("main包的功能nnn")
}

m2.go

package main // 声明自己归属于main包

import "fmt" // 导入内置包

func mmm(){
    fmt.Println("main包的功能mmm")
}

m1.go

package main // 声明自己归属于main包

func main(){
    // 自己包的功能直接调用即可,无需加任何前缀
    mmm()
    nnn()
}

2、包是一个不可分割的整体,是编译和归档的基本单位

sh-3.2# cd /test3/
sh-3.2# go build -o gogogo m3.go m2.go m1.go # 选项-o指定编译后得到的文件名字

编译main包后会得到一个可执行文件,而编译其他包的源代码则不会得到可执行文件。main包中的main函数是整个go程序的入口

sh-3.2# ls # 生成可执行文件gogogo
googo   m1.go   m2.go   m3.go
sh-3.2# ./googo 
main包的功能mmm
main包的功能nnn

三 标识符的可见性

​ 在上一小节我们已验证过了:如果是同一个包内源码文件想要访问彼此声明的标识符/名字(如变量、常量、类型、函数等),不需要经过任何处理,甚至不需要加任何前缀就可以直接访问,因为大家本就是一个整体。

​ 但如果是被其他包的源码文件导入使用,即想要跨包访问标识符/名字时,只有首字母大写的标识符/名字才能被导入者访问到,我们以自定义包为例,示范如下

package mypkg

import "fmt"

// 首字母无论大小写,包内的源码文件中都可以使用
var xxx = 100 // 包外部的导入者无法访问xxx

const Yyy = 200 // 外部的导入者可以访问Yyy

// 包外部的导入者无法访问teacher,更别提其内部的字段或方法了,此处指的是name
type teacher struct { 
    name string
}

// 包外部的导入者可以访问Student,进而可以访问到其内部字段Student,但无法访问字段class
type Student struct {
    Name  string 
    class string 
}

// 包外部的导入者可以访问Payer,进而可以访问到其内部的方法Pay,但无法访问方法init
type Payer interface {
    init() 
    Pay()  
}

// 外部的导入者可以访问Add
func Add(x, y int) int {
    return x + y
}

// 外部的导入者无法访问aeg
func age() { 
    var Age = 18 // 函数局部变量,只能在当前函数内使用,无论大小写,外部导入者肯定访问不到
    fmt.Println(Age)
} 

四 导入包语法

4.1 基本语法

在源码文件中导入其他包的基本语法如下

import "导入路径"

注意事项:

  • import导入语句通常放在文件开头包声明语句的下面。
  • ”导入路径“需要使用双引号包裹起来,指的是包所在的文件夹路径,必须唯一(详解go help importpath),go工具链与导入路径的格式息息相关,比如不同的路径对应着不同的检索位置,go工具链在解析程序中的导入语句时,会根据唯一的导入路径确定对应的检索位置,此处暂作了解,随后我们将详细介绍
    • 1、格式一:import path,常规导入路径
    • (I)内置包固定被放置在$GOROOT/src/下,内置包的导入路径是从src后开始计算的
      • 比如$GOROOT/src/fmt/ 导入方式为import "fmt"
    • (II)在未启用modules模式前,自定义或者第三方包需要放置在$GOPATH/src/下,包的导入路径同样是从src之后开始计算的,
      • 比如$GOPATH/src/foo/ ,导入方式为import "foo"
      • 比如$GOPATH/src/github.com/Egon/pro1/mypkg/,导入方式为import "github.com/Egon/pro1/mypkg"
    • 2、格式二:Relative import paths,相对导入路径(详见go help importpath,了解即可)
    • 3、格式三:Remote import paths,远程导入路径
    • 比如:import "github.com/google/go-cmp/cmp"
  • Go语言中禁止循环导入包。

4.2 单行导入

import "包1的导入路径"
import "包2的导入路径"
import "包3的导入路径"

4.3 多行导入

import (
    "包1的导入路径"
    "包2的导入路径"
    "包3的导入路径"
)

4.4 为导入的包起别名

为导入的包起别名在两种场景下非常有用
// 场景一:
之前我们提过包名和包的文件夹名与可以不一样,但为了方便操作,通常设置成一样的,下述导入路径对应的包名也正是balabalabalabala
import "github.com/Egon/pro1/balabalabalabala"

所以在使用时,”包名.属性“就成了"balabalabalabala.属性",这个前缀非常拖沓,我们可以为导入的包起个别名,注意是为导入的包起别名,不是给导入的路径起别名
import mmm "github.com/Egon/pro1/balabalabalabala"

这样使用时我们的”包名.属性“就替换成"mmm.属性",包名再长也无伤大雅

// 场景二:
当我们下载的多个第三方包包名冲突时,同样可以使用这种起别名的方式解决问题

4.5 点操作

import . “fmt”

使用时必须去掉前缀"fmt."而直接使用该包下内容,比如Println("hello bigEgon")

4.6 匿名导入与init

​ GO语言要求导入的包必须使用,如果我们只希望导入包,而并不想使用包内部的属性时,可以使用匿名导入包

import _ "导入路径"

​ 匿名导入的包与其他方式导入的包一样都会被编译到可执行文件中。

​ 匿名导入包主要目的是为了在导入时加载包内的一些配置,这就用到了包内的init()函数.所有包的init()函数会自动执行,不能在代码中主动调用它。

init()函数与main函数一样没有参数也没有返回值。

main函数所在的包同样也是一个包,但凡是包,都会先执行init()函数,所以Init()会在main函数前被调用

思考一个问题,如果是在main包中导入了其他的包,其他的包中又导入了其他的包……,这种情况下每个包的init执行顺序是什么样子呢?请看下图

注意:

1、同一个包下的源码文件是一个整体,他们声明的标识符/名字不能重名,但是init函数是个例外,同一个包下的多个源码文件中都可以有自己的init函数。由于同一个包下的多个源码文件地位相同,所以其init的执行顺序无关紧要。

2、如果包中含有多个.go源文件,它们将按照发给编译器的顺序进行初始化,Go语言的构建工具首先会将.go文件根据文件名排序,然后依次调用编译器编译

3、有时一个包会被多个包同时导入,那么它只会被导入一次(例如很多包可能都会用到fmt包,但它只会被导入一次,因为没有必要导入多次)

五 源码文件与包分类

​ 细分的话,源码文件其实可以分为三类

// 1、命令源码文件
main包所组织的源码文件是用来被编译成可执行命令的,称之为命令源码文件

// 2、库源码文件
main包之外的包是用来被导入使用的,其所组织的源码文件称之为库源码文件,编译之后不会得到任何可执行文件

// 3、测试源码文件
测试源码文件:名称以 _test.go 为后缀的源码文件,并且必须包含 Test 或者 Benchmark 名称前缀的函数。并且该函数接受一个类型为*testing.T或   *testing.B的参数
例如:
功能测试函数
func TestFind(t *testing.T){
    // 省略若干语句
}
性能测试函数
func BenchmarkFind(t *testing.B){
    // 省略若干语句
}

// 总结
命令源码文件、库源码文件  => Go语言程序文件
测试源码文件            => 辅助源码文件

​ 在Go中,源码文件必须以包的方式组织,包大致分为两大类:

main包:是我们的主程序,可以放在任意位置

库包(自定义包or第三方包):是主程序依赖的包,这就涉及到了包的依赖管理问题

六 包依赖性管理模式

6.1 modules模式的由来

​ 对于开发一个工程级项目,全部代码放入一个文件中肯定是不合理的,我们必然要分文件组织代码,为了管理文件,有了包这一代码管理组织形式。理论上只有一个main包可以的,包内的功能或属性直接访问就好了,而且main包可以放在任意文件夹下。但实际情况下,这么做是不合理的,因为我们开发过程中肯定不会所有功能都自己开发,肯定要引用/依赖其他的包,比如内置的、自定义的或者下载第三方的包,那么这些包都存放在哪里,导入的时候又会去哪里检索这些包呢???最开始go采用就只是单纯的GOPATH模式来管理依赖

// 一:了解
GOPATH模式由包go/build(go环境安装路径/src/go/build)实现并记录在其中的。GOPATH模式下,环境变量GOPATH的设置主要用于解析导入语句,

// 二:在GOPATH模式下,针对如下配置
export GOROOT=/usr/local/go //列出的是GO环境内置包的检索位置
export GOPATH=/var/root/go1:/var/root/go2:/var/root/go3 //列出的是第三方或自定义包的检索位置

// 三:包放置位置,见第一小节第(2)部分

// 四:有导入语句import "package1",包"pakcage1"的检索目录依次为
1、/usr/local/go/src
2、/var/root/go1/src
3、/var/root/go2/src
4、/var/root/go3/src

​ 但是随着大家使用go开发程序,慢慢发现单纯的GOPATH模式在版本依赖方面的管理简直令人作呕

// 引入
GOPATH可以设置多个路径,go get命令下载的第三方包默认都会放在GOPATH指定的第一个路径下的src中。当然我们可以将刚刚下载好的第三方包移动到任意GOPATH的其他路径下,但这改变不了GOPATH的检索顺序。导入该第三方包时总会依次检索GOPATH指定路径下的src,然后在某一个工作区路径下的src中找到。

// 问题
此时,如果我们基于go开发了三个不同的项目,但是这三个不同的项目依赖的是同一个第三方包的不同版本,如何管理???

// 尝试解决问题的思考
单纯用go get去下载该第三方包,由于默认存放的路径一样,所以新版本总是会覆盖老版本。
你可能会想到一个奇葩的解决方案:我们每下载好一个版本就将其移动到$GOPATH/src下一个单独命名的文件夹里,这样同一个包的三个版本的导入路径就不会冲突啦,如果包名一样,我们在导入时起个别名就ok了,没问题,我从来没说过GOPATH模式在依赖管理方面不行,我只是说它恶心。
改着改着你就疯了,因为这种依赖问题有可能涉及到诸多第三方包,难道每个都改一下,试问,你真的不觉得恶心吗?

​ 于是从Go1.5版本之后开始引入vendor模式来控制Go语言程序编译时依赖包搜索路径的优先级。比如查找项目的某个依赖包,首先会在项目根目录下的vender文件夹中查找,如果没有找到就会去$GOAPTH/src目录下查找。

​ vendor模式只是一个过渡时期,随着进一步发展,在Go1.11版本之后Go官方推出了modules模式(详见go help modules),并且从Go1.13版本开始,go modules将是Go语言默认的依赖管理工具。到今天Go1.14版本推出之后Go modules 功能已经被正式推荐在生产环境下使用了,所以此时此刻我们只需要潜心研究modules模式即可。

6.2 modues模式的开启

​ 环境变量GO111MODULE的配置决定了modules模式的开启和关闭,阅读下述内容,了解即可

GO111MODULE有三个可选值:off、on、auto,默认值是auto。

// 1、关闭modules模式:GO111MODULE=off 
(1)、使用的模式:
    此时go命令使用的仍是GOPATH模式

    注意:
    1、此时环境变量GOPATH很重要,因为其决定了依赖的自定义or第三方包的存放位置,与依赖检索息息相关

(2)、代码组织结构:
    main包:
        任意位置
    依赖包
        内置包:安装目录下/src中
        自定义或第三方包:必须放置在$GOPATH/src或者vendor目录下

(3)、依赖包检索:
    依据GOPATH模式,先去vendor目录下查找,然后去$GOPATH/src下查找(这已经是过去式了,了解即可)。

// 2、开启modules模式:GO111MODULE=on 
(1)、使用的模式:
    此时go命令使用的是modules模式,官网称其为’module-aware‘模块感知或‘module-aware mode’模块感知模式

    注意:
    1、此时环境变量GOPATH无关紧要,使用默认的配置就好,因为其根本不再参与依赖包的检索
    2、一旦开启modules模块式,则必须创建go.mod文件,每创建一个go.mod文件就声明了一个模块,该文件记录了依赖包的详细信息并用于检索

(2)、代码组织结构:
    仓库->模块->包(main包及自定义包,当然也可以是手动下载的第三方包)->源码文件,详见下一小节

    基于go命令下载的第三方依赖包(非手动):默认被放置到GOPATH指定的第一个路径的pkg/mod/目录下,同样被组织到了一个模块module里,该目录称之为go mod命令的缓存目录,多个项目共享该缓存目录(go clean -modcache 清除缓存)。所以,要知道的是,在模块感知模式下,GOPATH中的路径虽然不参与依赖的检索了,但还是有其用途的,并且在未设置GOBIN环境变量的情况下,go install安装的命令默认被放置到GOPATH指定的第一个路径的bin目录下,所以说,其实在模块感知模式下,GOPATH的路径使用默认的那一个路径就好了,程序员无需手动设置。

(3)、依赖包检索:
    有导入语句import "aaa/bbb"
    1、GO会优先去$GOROOT/src下查找文件夹“aaa/bbb"
    2、如果1没有找到,再依据go.mod的配置信息去查找文件夹"aaa/bbb"

// 3、动态选择是否启用modules模式:GO111MODULE=auto
    如果未设置GO111MODULE,默认值就是auto
    auto的意思是:go命令会根据当前目录启用或禁用moduless模式。仅当当前目录或当前目录的祖先目录中包含go.mod文件时,才启用模块支持。否则仍使用GOPATTH模式

​ 基于上述描述我们得知,要让go命令启用modules模式支持,我们其实无需配置环境变量GO111MODULE,但必须要做的是创建go.mod文件,该文件内记录着当前模块依赖包(自己模块内的包或其他模块中的包)的详细信息,是解决我们之前提及依赖性管理的核心所在,让我们一点一点来解开其神秘面纱。

6.3 modues模式下代码的四种组织形式与结构

联系管理员微信tutu19192010,注册账号

上一篇
下一篇
Copyright © 2022 Egon的技术星球 egonlin.com 版权所有 帮助IT小伙伴学到真正的技术