本篇博客主要介绍了如何从零开始,使用Go Module作为依赖管理,基于Gin来一步一步搭建Go的Web服务器。并使用Endless来使服务器平滑重启,使用Swagger来自动生成Api文档。

源码在此处:项目源码

大家可以先查看源码,然后再根据本篇文章,来了解搭建过程中服务器的一些细节。

搭建环境

以下所有的步骤都基于MacOS。

安装go

在这里推荐使用homebrew进行安装。当然你也可以使用源码安装。

brew install go

跑完命令之后,在命令行输入go。如果在命令行看到如下输出,则代表安装成功。

Go is a tool for managing Go source code.
Usage:
go <command> [arguments]
The commands are:
...
...

需要注意的是,go的版本需要在1.11之上,否则无法使用go module。以下是我的go的版本。

go version
# go version go1.12.5 darwin/amd64

IDE

推荐使用GoLand

配置GOPATH

打开GoLand,在GoLand的设置中找到Global GOPATH,将其设置为$HOME/go$HOME目录就是你的电脑的用户目录,如果该目录下没有go目录的话,也不需要新建,当我们在后面的操作中初始化模块的时候,会自动的在用户目录下新建go目录。

启用GO Module

同样,在GoLand中设置中找到Go Modules (vgo)。勾选Enable Go Modules (vgo) integration前的选择框来启用Go Moudle

搭建项目框架

新建目录

在你常用的工作区新建一个目录,如果你有github的项目,可以直接clone下来。

初始化go module

go mod init $MODULE_NAME

在刚刚新建的项目的根目录下,使用上述命令来初始化go module。该命令会在项目根目录下新建一个go.mod的文件。

如果你的项目是从github上clone下来的,$MODULE_NAME这个参数就不需要了。它会默认为github.com/$GITHUB_USER_NAME/$PROJECT_NAME

例如本项目就是github.com/detectiveHLH/go-backend-starter;如果是在本地新建的项目,则必须要加上最后一个参数。否则就会遇到如下的错误。

go: cannot determine module path for source directory /Users/hulunhao/Projects/go/test/src (outside GOPATH, no import comments)

初始化完成之后的go.mod文件内容如下。

module github.com/detectiveHLH/go-backend-starter

go 1.12

新建main.go

在项目的根目录下新建main.go。代码如下。

package main

import (
"fmt"
) func main() {
fmt.Println("This works")
}

运行main.go

在根目录下使用go run main.go,如果看到命令行中输出This works则代表基础的框架已经搭建完成。接下来我们开始将Gin引入框架。

引入Gin

Gin是一个用Go实现的HTTP Web框架,我们使用Gin来作为starter的Base Framework。

安装Gin

直接通过go get命令来安装

go get github.com/gin-gonic/gin

安装成功之后,我们可以看到go.mod文件中的内容发生了变化。

并且,我们在设定的GOPATH下,并没有看到刚刚安装的依赖。实际上,依赖安装到了$GOPATH/pkg/mod下。

module github.com/detectiveHLH/go-backend-starter

go 1.12

require github.com/gin-gonic/gin v1.4.0 // indirect

同时,也生成了一个go.sum文件。内容如下。

github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3 h1:t8FVkw33L+wilf2QiWkw0UV77qRpcH/JHPKGpKa2E8g=
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
github.com/gin-gonic/gin v1.4.0 h1:3tMoCCfM7ppqsR0ptz/wi1impNpT7/9wQtMZ8lr1mCQ=
github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/ugorji/go v1.1.4 h1:j4s+tAvLfL3bZyefP2SEWmhBzmuIlH/eqNuPdFPgngw=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ=
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

用过Node的人都知道,在安装完依赖之后会生成一个package-lock.json文件,来锁定依赖的版本。以防止后面重新安装依赖时,安装了新的版本,但是与现有的代码不兼容,这会带来一些不必要的BUG。

但是这个go.sum文件并不是这个作用。我们可以看到go.mod中只记录了一个Gin的依赖,而go.sum中则有非常多。是因为go.mod中只记录了最顶层,就是我们直接使用命令行安装的依赖。但是要知道,一个开源的包通常都会依赖很多其他的依赖包。

而go.sum就是记录所有顶层和其中间接依赖的依赖包的特定版本的文件,为每一个依赖版本生成一个特定的哈希值,从而在一个新环境启用该项目时,可以做到对项目依赖的100%还原。go.sum还会保留一些过去使用过的版本的信息。

在go module下,不需要vendor目录来保证可重现的构建,而是通过go.mod文件来对项目中的每一个依赖进行精确的版本管理。

如果之前的项目用的是vendor,那么重新用go.mod重新编写不太现实。我们可以使用go mod vendor命令将之前项目所有的依赖拷贝到vendor目录下,为了保证兼容性,在vendor目录下的依赖并不像go.mod一样。拷贝之后的目录不包含版本号。

而且通过上面安装gin可以看出,通常情况下,go.mod文件是不需要我们手动编辑的,当我们执行完命令之后,go.mod也会自动的更新相应的依赖和版本号。

下面我们来了解一下go mod的相关命令。

  • init 初始化go module
  • download 下载go.mod中的依赖到本地的缓存目录中($GOPATH/pkg/mod)下
  • edit 编辑go.mod,通过命令行手动升级和获取依赖
  • vendor 将项目依赖拷贝到vendor下
  • tidy 安装缺少的依赖,舍弃无用的依赖
  • graph 打印模块依赖图
  • verify 验证依赖是否正确

还有一个命令值得提一下,go list -m all可以列出当前项目的构建列表。

修改main.go

修改main.go的代码如下。

package main

import (
"fmt"
"github.com/gin-gonic/gin"
) func main() {
fmt.Println("This works.")
r := gin.Default()
r.GET("/hello", func(c *gin.Context) {
c.JSON(200, gin.H{
"success": true,
"code": 200,
"message": "This works",
"data": nil,
})
})
r.Run()
}

上述的代码引入了路由,熟悉Node的应该可以看出,这个与koa-router的用法十分相似。

启动服务器

照着上述运行main.go的步骤,运行main.go。就可以在控制台看到如下的输出。

This works.
[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 /hello --> 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

此时,服务器已经在8080端口启动了。然后在浏览器中访问http://localhost:8080/hello,就可以看到服务器的正常返回。同时,服务器这边也会打印相应的日志。

[GIN] 2019/06/08 - 17:41:34 | 200 |     214.213µs |             ::1 | GET      /hello

构建路由

新建路由模块

在根目录下新建router目录。在router下,新建router.go文件,代码如下。

package router

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

func InitRouter() *gin.Engine {
router := gin.New()
apiVersionOne := router.Group("/api/v1/")
apiVersionOne.GET("hello", func(c *gin.Context) {
c.JSON(200, gin.H{
"success": true,
"code": 200,
"message": "This works",
"data": nil,
})
})
return router
}

在这个文件中,导出了一个InitRouter函数,该函数返回gin.Engine类型。该函数还定义了一个路由为/api/v1/hello的GET请求。

在main函数中引入路由

将main.go的代码改为如下。

package main

import (
"fmt"
"github.com/detectiveHLH/go-backend-starter/router"
) func main() {
r := router.InitRouter()
r.Run()
}

然后运行main.go,启动之后,访问http://localhost:8080/api/v1/hello,可以看到,与之前访问/hello路由的结果是一样的。

到此为止,我们已经拥有了一个拥有简单功能的Web服务器。那么问题来了,这样的一个开放的服务器,只要知道了地址,你的服务器就知道暴露给其他人了。这样会带来一些安全隐患。所以我们需要给接口加上鉴权,只有通过认证的调用方,才有权限调用服务器接口。所以接下来,我们需要引入JWT。

引入JWT鉴权

使用go get命令安装jwt-go依赖。

go get github.com/dgrijalva/jwt-go

新建jwt鉴权文件

在根目录下新建middleware/jwt目录,在jwt目录下新建jwt.go文件,代码如下。

package jwt

import (
"github.com/detectiveHLH/go-backend-starter/consts"
"github.com/gin-gonic/gin"
"net/http"
"time"
) func Jwt() gin.HandlerFunc {
return func(c *gin.Context) {
var code int
var data interface{} code = consts.SUCCESS
token := c.Query("token")
if token == "" {
code = consts.INVALID_PARAMS
} else {
claims, err := util.ParseToken(token)
if err != nil {
code = consts.ERROR_AUTH_CHECK_TOKEN_FAIL
} else if time.Now().Unix() > claims.ExpiresAt {
code = consts.ERROR_AUTH_CHECK_TOKEN_TIMEOUT
}
} if code != consts.SUCCESS {
c.JSON(http.StatusUnauthorized, gin.H{
"code": code,
"msg": consts.GetMsg(code),
"data": data,
}) c.Abort()
return
} c.Next()
}
}

引入常量

此时,代码中会有错误,是因为我们没有声明consts这个包,其中的变量SUCCESS、INVALID_PARAMS和ERROR_AUTH_CHECK_TOKEN_FAIL是未定义的。根据code获取服务器返回信息的函数GetMsg也没定义。同样没有定义的还有util.ParseToken(token)和claims.ExpiresAt。所以我们要新建consts包。我们在根目录下新建consts目录,并且在consts目录下新建code.go,将定义好的一些常量引进去,代码如下。

新建const文件

const (
SUCCESS = 200
ERROR = 500
INVALID_PARAMS = 400
)

新建message文件

再新建message.go文件,代码如下。

var MsgFlags = map[int]string{
SUCCESS: "ok",
ERROR: "fail",
INVALID_PARAMS: "请求参数错误",
} func GetMsg(code int) string {
msg, ok := MsgFlags[code]
if ok {
return msg
}
return MsgFlags[ERROR]
}

新建util

在根目录下新建util,并且在util下新建jwt.go,代码如下。

package util

import (
"github.com/dgrijalva/jwt-go"
"time"
) var jwtSecret = []byte(setting.AppSetting.JwtSecret) type Claims struct {
Username string `json:"username"`
Password string `json:"password"`
jwt.StandardClaims
} func GenerateToken(username, password string) (string, error) {
nowTime := time.Now()
expireTime := nowTime.Add(3 * time.Hour)
claims := Claims{
username,
password,
jwt.StandardClaims {
ExpiresAt : expireTime.Unix(),
Issuer : "go-backend-starter",
},
}
tokenClaims := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
token, err := tokenClaims.SignedString(jwtSecret) return token, err
} func ParseToken(token string) (*Claims, error) {
tokenClaims, err := jwt.ParseWithClaims(token, &Claims{}, func(token *jwt.Token) (interface{}, error) {
return jwtSecret, nil
})
if tokenClaims != nil {
if claims, ok := tokenClaims.Claims.(*Claims); ok && tokenClaims.Valid {
return claims, nil
}
} return nil, err
}

新建setting包

在上面的util中,setting包并没有定义,所以在这个步骤中我们需要定义setting包。

使用go get命令安装依赖。

go get gopkg.in/ini.v1

在项目根目录下新建setting目录,并在setting目录下新建setting.go文件,代码如下。

package setting

import (
"gopkg.in/ini.v1"
"log"
) type App struct {
JwtSecret string
}
type Server struct {
Ip string
Port string
}
type Database struct {
Type string
User string
Password string
Host string
Name string
TablePrefix string
} var AppSetting = &App{}
var ServerSetting = &Server{}
var DatabaseSetting = &Database{}
var config *ini.File func Setup() {
var err error
config, err = ini.Load("config/app.ini")
if err != nil {
log.Fatal("Fail to parse 'config/app.ini': %v", err)
}
mapTo("app", AppSetting)
mapTo("server", ServerSetting)
mapTo("database", DatabaseSetting)
} func mapTo(section string, v interface{}) {
err := config.Section(section).MapTo(v)
if err != nil {
log.Fatalf("Cfg.MapTo RedisSetting err: %v", err)
}
}

新建配置文件

在项目根目录下新建config目录,并新建app.ini文件,内容如下。

[app]
JwtSecret = 233
[server]
Ip : localhost
Port : 8000
Url : 127.0.0.1:27017
[database]
Type = mysql
User = $YOUR_USERNAME
Password = $YOUR_PASSWORD
Host = 127.0.0.1:3306
Name = golang_test
TablePrefix = golang_test_

实现登录接口

新增登录接口

到此为止,通过jwt token进行鉴权的逻辑已经全部完成,剩下的就需要实现登录接口来将token在用户登录成功之后返回给用户。

使用go get命令安装依赖。

go get github.com/astaxie/beego/validation

在router下新建login.go,代码如下。

package router

import (
"github.com/astaxie/beego/validation"
"github.com/detectiveHLH/go-backend-starter/consts"
"github.com/detectiveHLH/go-backend-starter/util"
"github.com/gin-gonic/gin"
"net/http"
) type auth struct {
Username string `valid:"Required; MaxSize(50)"`
Password string `valid:"Required; MaxSize(50)"`
} func Login(c *gin.Context) {
appG := util.Gin{C: c}
valid := validation.Validation{}
username := c.Query("username")
password := c.Query("password") a := auth{Username: username, Password: password}
ok, _ := valid.Valid(&a)
if !ok {
appG.Response(http.StatusOK, consts.INVALID_PARAMS, nil)
return
} authService := authentication.Auth{Username: username, Password: password}
isExist, err := authService.Check()
if err != nil {
appG.Response(http.StatusOK, consts.ERROR_AUTH_CHECK_TOKEN_FAIL, nil)
return
} if !isExist {
appG.Response(http.StatusOK, consts.ERROR_AUTH, nil)
return
} token, err := util.GenerateToken(username, password)
if err != nil {
appG.Response(http.StatusOK, consts.ERROR_AUTH_TOKEN, nil)
return
} appG.Response(http.StatusOK, consts.SUCCESS, map[string]string{
"token": token,
})
}

新增返回类

在util包下新增response.go文件,代码如下。

package util

import (
"github.com/detectiveHLH/go-backend-starter/consts"
"github.com/gin-gonic/gin"
) type Gin struct {
C *gin.Context
} func (g *Gin) Response(httpCode, errCode int, data interface{}) {
g.C.JSON(httpCode, gin.H{
"code": httpCode,
"msg": consts.GetMsg(errCode),
"data": data,
}) return
}

新增鉴权逻辑

除了返回类,login.go中还有关键的鉴权逻辑还没有实现。在根目录下新建service/authentication目录,在该目录下新建auth.go文件,代码如下。

package authentication

import "fmt"

type Auth struct {
Username string
Password string
} func (a *Auth) Check() (bool, error) {
userName := a.Username
passWord := a.Password
// todo:实现自己的鉴权逻辑
fmt.Println(userName, passWord)
return true, nil
}

在此处,需要自己真正的根据业务去实现对用户调用接口的合法性校验。例如,可以根据用户的用户名和密码去数据库做验证。

修改router.go

修改router.go中的代码如下。

package router

import (
"github.com/detectiveHLH/go-backend-starter/middleware/jwt"
"github.com/gin-gonic/gin"
) func InitRouter() *gin.Engine {
router := gin.New() router.GET("/login", Login)
apiVersionOne := router.Group("/api/v1/") apiVersionOne.Use(jwt.Jwt()) apiVersionOne.GET("hello", func(c *gin.Context) {
c.JSON(200, gin.H{
"success": true,
"code": 200,
"message": "This works",
"data": nil,
})
})
return router
}

可以看到,我们在路由文件中加入了/login接口,并使用了我们自定义的jwt鉴权的中间件。只要是在v1下的路由,请求之前都会先进入jwt中进行鉴权,鉴权通过之后才能继续往下执行。

运行main.go

到此,我们使用go run main.go启动服务器,访问http://localhost:8080/api/v1/hello会遇到如下错误。

{
"code": 400,
"data": null,
"msg": "请求参数错误"
}

这是因为我们加入了鉴权,凡是需要鉴权的接口,都需要带上参数token。而要获取token则必须要先要登录,假设我们的用户名是Tom,密码是123。以此来调用登录接口。

http://localhost:8080/login?username=Tom&password=123

在浏览器中访问如上的url之后,可以看到返回如下。

{
"code": 200,
"data": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IlRvbSIsInBhc3N3b3JkIjoiMTIzIiwiZXhwIjoxNTYwMTM5MTE3LCJpc3MiOiJnby1iYWNrZW5kLXN0YXJ0ZXIifQ.I-RSi-xVV1Tk_2iBWolF1u94Y7oVBQXnHh6OI2YKJ6U"
},
"msg": "ok"
}

有了token之后,我们再调用hello接口,可以看到数据正常的返回了。

http://localhost:8080/api/v1/hello?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IlRvbSIsInBhc3N3b3JkIjoiMTIzIiwiZXhwIjoxNTYwMTM5MTE3LCJpc3MiOiJnby1iYWNrZW5kLXN0YXJ0ZXIifQ.I-RSi-xVV1Tk_2iBWolF1u94Y7oVBQXnHh6OI2YKJ6U

一般的处理方法是,前端拿到这个token,利用持久化存储存下来,然后之后的每次请求都将token写在header中发给后端。后端先通过header中的token来校验调用接口的合法性,验证通过之后才进行真正的接口调用。

而在这我将token写在了request param中,只是为了做一个例子来展示。

引入swagger

完成了基本的框架之后,我们就开始为接口引入swagger文档。写过java的同学应该对swagger不陌生。往常写API文档,都是手写。即每个接口的每一个参数,都需要手打。

而swagger不一样,swagger只需要你在接口上打上几个注解(Java中的操作),就可以自动为你生成swagger文档。而在go中,我们是通过注释的方式来实现的,接下来我们安装gin-swagger

安装依赖

go get github.com/swaggo/gin-swagger
go get -u github.com/swaggo/gin-swagger/swaggerFiles
go get -u github.com/swaggo/swag/cmd/swag
go get github.com/ugorji/go/codec
go get github.com/alecthomas/template

在router中注入swagger

引入依赖之后,我们需要在router/router.go中注入swagger。在import中加入_ "github.com/detectiveHLH/go-backend-starter/docs"

并在router := gin.New()之后加入如下代码。

router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))

为接口编写swagger注释

在router/login.go中的Login函数上方加上如下注释。

// @Summary 登录
// @Produce json
// @Param username query string true "username"
// @Param password query string true "password"
// @Success 200 {string} json "{"code":200,"data":{},"msg":"ok"}"
// @Router /login [get]

初始化swagger

在项目根目录下使用swag init命令来初始化swagger文档。该命令将会在项目根目录生成docs目,内容如下。

.
├── docs.go
├── swagger.json
└── swagger.yaml

查看swagger文档

运行main.go,然后在浏览器访问http://localhost:8080/swagger/index.html就可以看到swagger根据注释自动生成的API文档了。

引入Endless

安装Endless

go get github.com/fvbock/endless

修改main.go

package main

import (
"fmt"
"github.com/detectiveHLH/go-backend-starter/router"
"github.com/fvbock/endless"
"log"
"syscall"
) func main() {
r := router.InitRouter() address := fmt.Sprintf("%s:%s", setting.ServerSetting.Ip, setting.ServerSetting.Port)
server := endless.NewServer(address, r)
server.BeforeBegin = func(add string) {
log.Printf("Actual pid is %d", syscall.Getpid())
} err := server.ListenAndServe()
if err != nil {
log.Printf("Server err: %v", err)
}
}

写在后面

对比起没有go module的依赖管理,现在的go module更像是Node.js中的package.json,也像是Java中的pom.xml,唯一不同的是pom.xml需要手动更新。

当我们拿到有go module项目的时候,不用担心下来依赖时,因为版本问题可能导致的一些兼容问题。直接使用go mod中的命令就可以将制定了版本的依赖全部安装,其效果类似于Node.js中的npm install

go module定位module的方式,与Node.js寻找依赖的逻辑一样,Node会从当前命令执行的目录开始,依次向上查找node_modules中是否有这个依赖,直到找到。go则是依次向上查找go.mod文件,来定位一个模块。

相信之后go之后的依赖管理,会越来越好。

Happy hacking.

参考:

往期文章:

相关:

  • 个人网站: Lunhao Hu
  • 微信公众号: SH的全栈笔记(或直接在添加公众号界面搜索微信号LunhaoHu)

用go-module作为包管理器搭建go的web服务器的更多相关文章

  1. .NET持续集成与自动化部署之路第二篇——使用NuGet.Server搭建公司内部的Nuget(包)管理器

    使用NuGet.Server搭建公司内部的Nuget(包)管理器 前言     Nuget是一个.NET平台下的开源的项目,它是Visual Studio的扩展.在使用Visual Studio开发基 ...

  2. Node.js 包管理器 NPM 讲解

    包管理器又称软件包管理系统,它是在电脑中自动安装.配制.卸载和升级软件包的工具组合,在各种系统软件和应用软件的安装管理中均有广泛应用.对于我们业务开发也很受益,相同的东西不必重复去造轮子. 每个工具或 ...

  3. Bower => 前端开发也有包管理器

    摘要: 一直以来npm,pip等各种包管理器好像都和前端开发没什么太大关系,当然因为nodejs的原因可能感觉npm会亲切一些,不过终归不是针对客户端的包管理工作,所以Bower的出现确实让人眼前一亮 ...

  4. 华为云提供针对Nuget包管理器的缓存加速服务

    在Visual Studio 2013.2015.2017中,使用的是Nuget包管理器对第三方组件进行管理升级的.而且 Nuget 是我们使用.NET Core的一项基础设施,.NET的软件包管理器 ...

  5. Node.js_简介及其 npm 包管理器基本使用_npm_cnpm_yarn_cyarn

    Node.js 既是语言也是平台,跳过了 Apache.Nginx 等 HTTP 服务器,直接面向前端开发 JavaScript 是由 ECMAScript.文档对象模型(DOM)和浏览器对象模型(B ...

  6. Openresty 学习笔记(四)lualocks包管理器安装使用

    Luarocks是一个Lua包管理器,基于Lua语言开发,提供一个命令行的方式来管理Lua包依赖.安装第三方Lua包等,社区比较流行的包管理器之一,另还有一个LuaDist,Luarocks的包数量比 ...

  7. VS2013中Nuget程序包管理器控制台使用入门(二)-如何使用Nuget提供的帮助(原创)

    如何使用Nuget提供的帮助? 1.从get-help Nuget开始,键入“get-help NuGet”以查看所有可用的 NuGet 命令. 用法: PM> get-help Nuget 主 ...

  8. .NET世界的包管理器——Nuget

    NugetServer 使用指南 为什么要使用Nuget 在我们的项目, 存在着一些公共Dll, 这些Dll被大量的项目所引用.同时这些公共dll也同时在进行版本升级, 由于缺乏版本管理,这些Dll会 ...

  9. Kubernetes学习之路(二十五)之Helm程序包管理器

    目录 1.Helm的概念和架构 2.部署Helm (1)下载helm (2)部署Tiller 3.helm的使用 4.chart 目录结构 5.chart模板 6.定制安装MySQL chart (1 ...

随机推荐

  1. Iris入门操练1

    选一个框架,慢慢熟悉··· 按官网文档,先走一次.. package main import ( "github.com/kataras/iris/v12" "githu ...

  2. Tyvj 1953 Normal:多项式,点分治

    Decription: 某天WJMZBMR学习了一个神奇的算法:树的点分治! 这个算法的核心是这样的: 消耗时间=0 Solve(树 a) 消耗时间 += a 的 大小 如果 a 中 只有 1 个点, ...

  3. docker 指定版本rpm包安装

    1.docker rpm包下载地址 # https://download.docker.com/linux/centos/7/x86_64/stable/Packages/ 2.下载rpm包 # wg ...

  4. Deepnude算法“tuo”衣服

    PS:我不是偷窥狂.我是技术的爱好者 换脸视频后AI又出偏门应用:用算法“tuo”女性衣服 据美国科技媒体Motherboard报道,一名程序员最近开发出一款名叫DeepNude的应用,只要给Deep ...

  5. Format a Business Object Caption 设置业务对象标题的格式

    In this lesson, you will learn how to format the caption of a detail form that displays a business o ...

  6. 简约清新日系你好五月通用PPT模板推荐

    模版来源:http://ppt.dede58.com/peixunyanjiang/26488.html

  7. IDEA激活码

    MNQ043JMTU-eyJsaWNlbnNlSWQiOiJNTlEwNDNKTVRVIiwibGljZW5zZWVOYW1lIjoiR1VPIEJJTiIsImFzc2lnbmVlTmFtZSI6I ...

  8. js随机生成ID

    processID = () => { const uuid = 'xxxxxxxx-xxxx-xxxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function ...

  9. Android 进程间通讯方式

    Android 进程间通讯方式 1.通过单向数据管道传递数据 管道(使用PipedWriter/ 创建PipedReader)是java.io包的一部分.也就是说,它们是一般的Java功能,而不是An ...

  10. 剑指offer 28:字符串的排列

    题目描述 输入一个字符串,按字典序打印出该字符串中字符的所有排列.例如输入字符串abc,则打印出由字符a,b,c所能排列出来的所有字符串abc,acb,bac,bca,cab和cba. 输入描述 输入 ...