Golang入门-Golang包管理

言淦 · · 1538 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

Golang的包管理一直是广大开发者吐槽的点之一。

Go 包管理简史

Golang的包管理分为三个阶段,version < 1.11、 1.11 <= version < 1.13、 version >= 1.13。

version < 1.11

在这个阶段,Golang的包管理存在以下不足:

  • 必须设置GOPATH环境变量,且源代码必须存放在GOPATH下
  • 拉取外部依赖包时,总是拉取最新的版本,无法指定需要的版本

之所以设置GOPATH环境变量有两个原因:

  • 它规定了go get命令下载的依赖包的存储位置($GOPATH/src)
  • 通过设置GOPATH,可以方便Golang计算出import的路径

另外,由于无法指定依赖包的版本,因此容易导致“本地测试OK,但线上部署失败”的问题。这样的问题是广大开发者无法忍受的,所以,各种包管理工具开始涌现出来,典型的有dep,glide等,这里不再赘述。

1.11 <= version < 1.13

这个阶段默认使用的还是GOPATH的管理方式,但是开始支持Go Module的管理方式。

Go Module解决了上述的阶段存在的不足:
1.它不再需要GOPATH,即你的项目代码可以随意存放
2.它通过go.mod + go.sum解决依赖包的版本问题(后面会讲到)

如果需要迁移到Go Module,需要设置以下环境变量

vim ~/.bash_profile

export GO111MODULE=on
复制代码

version >= 1.13

从这个阶段开始,Golang的包管理默认使用的是Go Module。

使用GOPATH进行包管理

注:为了完整性,这里尝试使用go 1.11复现之前使用GOPATH进行包管理的情况。

1.下拉docker镜像

$ docker pull ubuntu:16.04

$ docker run -itd --name golang-lab ubuntu:16.04 /bin/bash

$ apt-get update && apt-get install wget

复制代码

2.安装go 1.11

$ wget https://dl.google.com/go/go1.11.10.linux-amd64.tar.gz

$ tar -zxvf go1.11.10.linux-amd64.tar.gz

$ go/bin/go version
go version go1.11.10 linux/amd64

复制代码

3.新建项目
3.1 这里我们假定/home/go-projects为我们的工作区
3.2 新建bin目录用于存放可执行文件; 新建pkg目录用于存放静态链接库文件; 新建src目录用于存放的我们源码文件, 一般我们写的代码都会放到这个目录下。
3.3 git.own.com 名称可自定义,这里只是个人编程习惯,表示这里存放的都是个人项目

$ mkdir /home/go-projects

$ cd /home/go-projects && mkdir src && mkdir pkg && mkdir bin

$ cd src && mkdir git.own.com && cd git.own.com

$ mkdir gopath-lab && cd gopath-lab && touch main.go

复制代码

4.目录树

root@ebca4ae962aa:/home/go-projects# tree -L 4
.
|-- bin
|-- pkg
`-- src
    `-- git.own.com
        `-- gopath-lab
            `-- main.go
复制代码

5.设置环境变量

  • GOPATH:工作区路径,存放源代码。
  • GOBIN:当使用go install xx.go 时, 生成的可执行文件就会放在此目录
  • GOROOT:Go的安装位置,用于寻找标准库,这里是/home/go
$ vim ~/.bashrc
export PATH=$PATH:/home/go/bin

export GOPATH=/home/go-projects
export GOBIN=/home/go-projects/bin
export GOROOT=/home/go

复制代码

如果没有设置GOBIN,会报错

$ go install main.go 
go install: no install location for .go files listed on command line (GOBIN not set)
复制代码

6.main.go 代码如下:

package main

import "github.com/gin-gonic/gin"

func main() {
	r := gin.Default()
	r.GET("/ping", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "pong",
		})
	})
	r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
}

复制代码

可以看到,直接go run并不能自动下载依赖

$ go run main.go 
main.go:3:8: cannot find package "github.com/gin-gonic/gin" in any of:
        /home/go/src/github.com/gin-gonic/gin (from $GOROOT)
        /home/go-projects/src/github.com/gin-gonic/gin (from $GOPATH)
复制代码

7.手动下载并测试

# 居然奇迹般下载成功了,一般这个时候需要设置代理
$ go get -v github.com/gin-gonic/gin

# 可以看到,源码已经下载到src目录了
$ ls /home/go-projects/src/
git.own.com  github.com  golang.org  gopkg.in

# 再次执行,运行成功
$ go run main.go 
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /ping                     --> main.main.func1 (3 handlers)
[GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
[GIN-debug] Listening and serving HTTP on :8080
复制代码

使用Go Module进行包管理

本节翻译自《Using Go Modules》

Module 是一系列依赖包的集合,通过go mod init xxx可初始化一份空的go.mod和go.sum,这两份文件存放于项目的根路径下。

对于go.mod,它不仅存储了这些依赖包的路径及其版本,同时也指定了import的根路径,对于go.sum,它存放了依赖包内容的预期校验和,保证前一次下载的代码和现在下载的代码是一致的。

配置代理

由于Golang大部分依赖包都在国外,直接下载非常缓慢,在没有Go Module的时候,需要自己配置代理,比如socks;但是有了Go Module,就可通过设置环境变量来配置代理了,具体参考:goproxy.io/zh/。

配置时有几个注意点:
1.如果你有私有仓库和公共仓库,则需要加上direct参数,并配置GOPRIVATE(针对Go1.13)

# 有了direct,GOPRIVATE指定的仓库不会使用代理
go env -w GOPROXY=https://goproxy.io,direct

# 设置不走代理的私有仓库,多个用逗号相隔
go env -w GOPRIVATE=*.corp.example.com
复制代码

2.如果你使用的是Golang IDE,则注意该IDE也要配置

3.如果你的~/.bash_profile或~./bashrc 文件存在GO111MODULE等环境变量,则go env 写入时会冲突

warning: go env -w GOPROXY=... does not override conflicting OS environment variable

初始化项目

1.新建文件夹

mkdir go-module-lab && cd go-module-lab

2.初始化Go Module项目,git.own.com/go-module是自定义的

go mod init git.own.com/go-module

3.查看go.mod

module git.own.com/go-module

go 1.13
复制代码

添加代码测试

1.自定义库

mkdir hello && touch hello/hello.go

hello.go 内容

package hello

func Hello() string {
	return "Hello, world."
}
复制代码

2.新建main.go测试,内容如下

package main

import (
	"fmt"
	// 前面提过,go.mod 指定了import时的根路径
	"git.own.com/go-module/hello"
)

func main()  {
	fmt.Println(hello.Hello())
}
复制代码

添加外部依赖

1.更新hello.go文件,引入rsc.io/quote依赖

package hello

import "rsc.io/quote"

func Hello() string {
    return quote.Hello()
}
复制代码

2.执行go run main.go,会自动下载依赖

➜  go-module-lab go run main.go 
go: finding rsc.io/quote v1.5.2
go: downloading rsc.io/quote v1.5.2
go: extracting rsc.io/quote v1.5.2
go: downloading rsc.io/sampler v1.3.0
go: extracting rsc.io/sampler v1.3.0
go: downloading golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
go: extracting golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
go: finding rsc.io/sampler v1.3.0
go: finding golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c

Hello, world.
复制代码

3.查看go.mod

module git.own.com/go-module

go 1.13

require rsc.io/quote v1.5.2
复制代码

可以看到,使用Go Module的包管理方式,Golang会自动帮我们处理包的依赖关系,并把缺失的包添加到go.mod,并使用rsc.io/quote的最新版本。(这里的最新版本应理解为最新并打了tag的版本,如果没有打tag,则会使用一种pseudo-version的方式标识,下文会说到)

4.借助go list命令查看所有依赖

$ go list -m all
git.own.com/go-module
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
rsc.io/quote v1.5.2
rsc.io/sampler v1.3.0
复制代码

补充:
pseudo-versions(伪版本)
一般情况下,go.mod使用语义化版本来标志依赖包的版本号,比如v1.0.0、v1.0.1。

它包含三个部分:

  • 主版本号:当你做了不兼容的 API 修改,比如v1.5.2的1
  • 次版本号:当你做了向下兼容的功能性新增,比如v1.5.2的5
  • 修订号:当你做了向下兼容的问题修正,比如1.5.2的2

语义化版本规定,同一个主版本号的必须向下兼容,比如v1.5.2必须向下兼容v1.1.0;如果代码不兼容,则必须使用新的版本号。

但是语义化版本是基于项目有打tag的情况下,如果一些项目没有打tag,则Golang会使用一种pseudo-version来标识,类似v0.0.0-yyyymmddhhmmss-abcdefabcdef的形式。

其中,yyyymmddhhmmss使用的是UTC时间,abcdefabcdef对应的是你这次commit的哈希值(前12位),

对于前缀v0.0.0,则有三种情况:
1.当你的项目一个tag都没有的时候,形式为v0.0.0-yyyymmddhhmmss-abcdefabcdef

2.当你项目最近打的tag的名称为vX.Y.Z-pre的时候,形式为vX.Y.Z-pre.0.yyyymmddhhmmss-abcdefabcdef

3.当你的项目最近打的tag的名称是vX.Y.Z的时候,形式为vX.Y.(Z+1)-0.yyyymmddhhmmss-abcdefabcdef

参考:golang.org/cmd/go/#hdr…

go.sum
之所以有go.sum文件,是因为单纯地通过语义化版本(v1.5.2)无法确定每次通过v1.5.2标签下载的都是同一份代码。

比如发布者在 GitHub 上给自己的项目打上 v1.5.2 的tag之后,依旧可以删掉这个tag ,提交不同的内容后再重新打个 1.5.2 的 tag。

为了确定是否是同一份代码,go.sum存放了特定模块版本的内容的预期校验和,如果该代码有改动,则预期校验和不匹配,就会导致编译错误。

verifying xxx/base@v1.3.0: checksum mismatch
	downloaded: h1:T2eK+D0jzzeu4+S+oP9KvGgovPnl4FjxYShqdNSPrjc=
	go.sum:     h1:Crwm2FliMjZ3BABjnydOpoJiFPaKcod/zYNOtcB9Xkw=
复制代码

更新外部依赖

更新次版本号

更新次版本号比较简单,直接使用go get即可,比如更新golang.org/x/text

go get golang.org/x/text

通过查看go.mod的变化,我们可以看到golang.org/x/text的版本号由v0.0.0-20170915032832-14c0d48ead0c升级到v0.3.2。(indirect表明该依赖包在源码中没有用到,是间接依赖的)

module git.own.com/go-module

go 1.13

require (
        golang.org/x/text v0.3.2 // indirect
        rsc.io/quote v1.5.2
)
复制代码

除此之外,我们还可以更新到特定版本,在此之前,我们先看看该模块有哪些可用版本(以rsc.io/quote为例)

$ go list -m -versions rsc.io/quote  
rsc.io/quote v1.0.0 v1.1.0 v1.2.0 v1.2.1 v1.3.0 v1.4.0 v1.5.0 v1.5.1 v1.5.2 v1.5.3-pre1
复制代码

更新到特定版本:

go get rsc.io/quote@v1.4.0

如果想要使用特定的分支,只需要把版本号换成分支名即可(如果分支名包含特定符号,如"/",可用双引号将分支名括起来):

go get rsc.io/quote@dev

更新主版本号

如果需要更新主版本号,需要在代码中手动指定,因为不同主版本号相当于一个新的依赖(库)。

1.添加新函数

package hello

import (
	"rsc.io/quote"
	quoteV3 "rsc.io/quote/v3"  
)

func Hello() string {
	return quote.Hello()
}

func Proverb() string {
	return quoteV3.Concurrency()
}
复制代码

2.自动下载依赖

package main

import (
	"fmt"
	"git.own.com/go-module/hello"
)

func main()  {
	fmt.Println(hello.Hello())

	fmt.Println("proverb", hello.Proverb())
}
复制代码

3.查看go.mod

module git.own.com/go-module

go 1.13

require (
	golang.org/x/text v0.3.2 // indirect
	rsc.io/quote v1.5.2
	rsc.io/quote/v3 v3.1.0
	rsc.io/sampler v1.3.1 // indirect
)

复制代码

从上面可以看出,Go Module每一个主版本号使用不同的路径表示,如v1,v2,v3;另外,Golang允许同时存在多个主版本号,因为路径不同,相当于是一个新的库,这样做的目的是保持增量迁移。

比如我一开始使用rsc.io/quote,后面有改动,且与之前不兼容,这是我就可以使用新的主版本号,比如rsc.io/quote/v3,但是Hello这个函数暂时还不能迁移到V3版本,这是多版本的作用就凸显出来了

删除多余依赖

当过了一段时间,我们已经把把rsc.io/quote的代码全部迁移到新版本rsc.io/quote/v3, 类似下面的代码

package hello

import (
	quoteV3 "rsc.io/quote/v3"
)

func Hello() string {
	return quoteV3.HelloV3()
}

func Proverb() string {
	return quoteV3.Concurrency()
}

复制代码

这时之前的go.mod里面的rsc.io/quote是多余的,我们可以通过go mod tidy 删除多余的rsc.io/quote

$ go mod tidy

$ cat go.mod
module git.own.com/go-module

go 1.13

require (
	golang.org/x/text v0.3.2 // indirect
	rsc.io/quote/v3 v3.1.0
	rsc.io/sampler v1.3.1 // indirect
)

复制代码

总结

1.go mod init: 初始化一个Go Module项目,同时生成go.mod和go.sum文件
2.go build/go test/go run: 会自动下载依赖,并更新go.mod和go.sum文件
3.go list -m all:打印目前的所有依赖包
4.go get:手动下载依赖包,或者更改依赖包版本
5.go mod tidy:增加缺失的依赖,删除没有用到的依赖

其他命令

go env

配置一些环境变量。

# 环境变量说明文档
go help environment

# 环境变量配置文件路径
$ go env GOENV
/Users/xxx/Library/Application Support/go/env

# 列出所有环境变量
go env

# 列出所有环境变量(以json格式)
go env -json

# 修改某个环境变量
go env -w GOPROXY=https://goproxy.io,direct

# 重置某个变量
go env -u GOPROXY

复制代码

推荐阅读

Go语言包管理简史
初窥Go module
Go modules:版本是如何选择的?
谈谈go.sum


有疑问加站长微信联系(非本文作者)

本文来自:掘金

感谢作者:言淦

查看原文:Golang入门-Golang包管理

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1538 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传