golang api项目
在过去的几年中,我从事过几个用GO编写的项目。 我注意到开发人员面临的最大挑战是在项目布局方面缺乏约束或标准。 我想分享一些对我和我的团队最有效的发现和模式。 为了更好地理解,我将逐步完成创建简单的REST API的步骤。
mkdir -p $GOPATH /src/github.com/boilerplate/pkg \
$GOPATH /src/github.com/boilerplate/cmd \
$GOPATH /src/github.com/boilerplate/db/scripts \
$GOPATH /src/github.com/boilerplate/scripts
pkg /将包含通用/可重用的程序包, cmd /程序, db / scripts与db相关的脚本和scripts /将包含通用脚本。
如今,没有docker的应用程序就无法构建。 它使一切变得容易得多,因此我也将使用它。 我将尽量避免使初学者过分复杂,仅添加必要的内容:PostgreSQL数据库形式的持久层,将建立与数据库的连接的简单程序,在docker环境中运行,并在每次源代码更改时重新编译。 哦,差点忘了,我还将使用GO模块! 让我们开始吧:
$cd $GOPATH /src/github.com/boilerplate && \
go mod init github.com/boilerplate
让我们为本地开发环境创建一个Dockerfile.dev :
# Start from golang v1.13.4 base image to have access to go modules
FROM golang: 1.13 . 4
# create a working directory
WORKDIR /app
# Fetch dependencies on separate layer as they are less likely to
# change on every build and will therefore be cached for speeding
# up the next build
COPY ./go.mod ./go.sum ./
RUN go mod download
# copy source from the host to the working directory inside
# the
COPY . .
# This exposes port 7777 to the outside world
EXPOSE 7777
我既不想安装/设置PostgreSQL数据库,也不想让任何其他项目贡献者这样做。 让我们使用docker-compose自动化此步骤。 docker-compose.yml文件的内容:
version: "3.7"
volumes:
boilerplatevolume:
name: boilerplate-volume
networks:
boilerplatenetwork:
name: boilerplate-network
services:
pg:
image: postgres:12.0
restart: on-failure
env_file:
- .env
ports:
- "${POSTGRES_PORT}:${POSTGRES_PORT}"
volumes:
- boilerplatevolume: /var/lib/postgresql/data
- ./db/scripts:/docker-entrypoint-initdb.d/
networks:
- boilerplatenetwork
boilerplate_api:
build:
context: .
dockerfile: Dockerfile.dev
depends_on:
- pg
volumes:
- ./:/app
ports:
- 7777 :7777
networks:
- boilerplatenetwork
env_file:
- .env
entrypoint: ["/bin/bash", "./scripts/entrypoint.dev.sh" ]
我不会在这里解释docker-compose的工作原理,但这应该是可以自我解释的。 有两点需要指出。 首先是pg服务中的./db/scripts:/docker-entrypoint-initdb.d/
。 当我运行docker-compose时,pg服务将从主机./db/scripts
文件夹中获取bash脚本,并将其放置在pg容器中并运行。 当前只有一个脚本。 这将确保将创建测试数据库。 让我们创建该脚本文件:
$ touch ./db/scripts/1_create_test_db.sh
让我们看看该脚本的样子:
#!/bin/bash
set -e
psql -v ON_ERROR_STOP=1 --username " $POSTGRES_USER " --dbname " $POSTGRES_DB " <<-EOSQL
DROP DATABASE IF EXISTS boilerplatetest;
CREATE DATABASE boilerplatetest;
EOSQL
第二个有趣的事情是entrypoint: ["/bin/bash", "./scripts/entrypoint.dev.sh"]
它以不影响go.mod的方式安装CompileDaemon ,并且以后不会在生产环境中拾取和安装相同的软件包。 它还会构建我们的应用程序,开始侦听对源代码所做的任何更改并重新编译它。 看起来像这样:
#!/bin/bash
set -e
GO111MODULE=off go get github.com/githubnemo/CompileDaemon
CompileDaemon --build= "go build -o main cmd/api/main.go" -- command =./main
接下来,我将在项目的根目录中创建.env
文件, .env
文件将包含用于本地开发的所有环境变量:
POSTGRES_PASSWORD =password
POSTGRES_USER =postgres
POSTGRES_PORT = 5432
POSTGRES_HOST =pg
POSTGRES_DB =boilerplate
TEST_DB_HOST =localhost
TEST_DB_NAME =boilerplatetest
我们的pg服务将在docker-compose.yml中使用所有带有POSTGRES_
前缀的变量,并创建包含相关详细信息的数据库。
在下一步中,我将创建配置包,该包将加载,持久化并使用环境变量进行操作:
// pkg/config/config.go
package config
import (
"flag"
"fmt"
"os"
)
type Config struct {
dbUser string
dbPswd string
dbHost string
dbPort string
dbName string
testDBHost string
testDBName string
}
func Get () * Config {
conf := &Config{}
flag.StringVar(&conf.dbUser, "dbuser" , os.Getenv( "POSTGRES_USER" ), "DB user name" )
flag.StringVar(&conf.dbPswd, "dbpswd" , os.Getenv( "POSTGRES_PASSWORD" ), "DB pass" )
flag.StringVar(&conf.dbPort, "dbport" , os.Getenv( "POSTGRES_PORT" ), "DB port" )
flag.StringVar(&conf.dbHost, "dbhost" , os.Getenv( "POSTGRES_HOST" ), "DB host" )
flag.StringVar(&conf.dbName, "dbname" , os.Getenv( "POSTGRES_DB" ), "DB name" )
flag.StringVar(&conf.testDBHost, "testdbhost" , os.Getenv( "TEST_DB_HOST" ), "test database host" )
flag.StringVar(&conf.testDBName, "testdbname" , os.Getenv( "TEST_DB_NAME" ), "test database name" )
flag.Parse()
return conf
}
func (c *Config) GetDBConnStr () string {
return c.getDBConnStr(c.dbHost, c.dbName)
}
func (c *Config) GetTestDBConnStr () string {
return c.getDBConnStr(c.testDBHost, c.testDBName)
}
func (c *Config) getDBConnStr (dbhost, dbname string ) string {
return fmt.Sprintf(
"postgres://%s:%s@%s:%s/%s?sslmode=disable" ,
c.dbUser,
c.dbPswd,
dbhost,
c.dbPort,
dbname,
)
}
那么这里发生了什么? Config程序包具有一个公共Get
函数。 它创建一个指向Config实例的指针,尝试获取变量作为命令行参数,并使用env vars作为默认值。 因此,这是两全其美的选择,因为它使我们的配置非常灵活。 Config实例有2种方法来获取dev和测试数据库连接字符串。
接下来,让我们创建db软件包,该软件包将建立并保持与数据库的连接:
// pkg/db/db.go
package db
import (
"database/sql"
_ "github.com/lib/pq"
)
type DB struct {
Client *sql.DB
}
func Get (connStr string ) (*DB, error) {
db, err := get(connStr)
if err != nil {
return nil , err
}
return &DB{
Client: db,
}, nil
}
func (d *DB) Close () error {
return d.Client.Close()
}
func get (connStr string ) (*sql.DB, error) {
db, err := sql.Open( "postgres" , connStr)
if err != nil {
return nil , err
}
if err := db.Ping(); err != nil {
return nil , err
}
return db, nil
}
在这里,我介绍了另一个第三方软件包github.com/lib/pq
,您可以在此处了解更多信息。 再次,有一个公共的Get
函数,该函数接受连接字符串,建立与数据库的连接,并返回指向数据库实例的指针。
整个应用程序始终需要访问数据库和程序配置。 为了方便进行依赖注入,我将创建另一个程序包,该程序包将组装所有必需的构造块。
// pkg/application/application.go
package application
import (
"github.com/boilerplate/pkg/config"
"github.com/boilerplate/pkg/db"
)
type Application struct {
DB *db.DB
Cfg *config.Config
}
func Get () (*Application, error) {
cfg := config.Get()
db, err := db.Get(cfg.GetDBConnStr())
if err != nil {
return nil , err
}
return &Application{
DB: db,
Cfg: cfg,
}, nil
}
再次有公共的Get
函数,请记住,一致性是关键! :)它返回指向我们的Application实例的指针,该实例将保存我们的配置和对数据库的访问。
我想添加另一个服务来保护应用程序,侦听任何程序终止信号并执行清除操作,例如关闭数据库连接:
// pkg/exithandler/exithandler.go
package exithandler
import (
"log"
"os"
"os/signal"
"syscall"
)
func Init (cb func () ) {
sigs := make ( chan os.Signal, 1 )
terminate := make ( chan bool , 1 )
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
go func () {
sig := <-sigs
log.Println( "exit reason: " , sig)
terminate <- true
}()
<-terminate
cb()
log.Print( "exiting program" )
}
因此, exithandler具有公共Init
函数,该函数将接受回调函数,当程序意外退出或被用户终止时,将调用该回调函数。
现在所有基本构建块都已就绪,我终于可以将它们投入工作:
// cmd/api/main.go
package main
import (
"log"
"github.com/boilerplate/pkg/application"
"github.com/boilerplate/pkg/exithandler"
"github.com/joho/godotenv"
)
func main () {
if err := godotenv.Load(); err != nil {
log.Println( "failed to load env vars" )
}
app, err := application.Get()
if err != nil {
log.Fatal(err.Error())
}
exithandler.Init( func () {
if err := app.DB.Close(); err != nil {
log.Println(err.Error())
}
})
}
有一个新的第3方软件包引入了github.com/joho/godotenv
,它将从先前创建的.env文件中加载env var。 它将获得指向拥有配置和数据库连接的应用程序的指针,并侦听任何中断以执行正常关闭。
采取行动的时间:
$ docker-compose up --build
好的,现在该应用程序正在运行,我想确保可以使用2个数据库。 我将通过键入以下命令列出所有正在运行的Docker容器:
$ docker ls
我可以在名称列中分配pg servide名称。 就我而言,码头工人已将其命名为boilerplate_pg_1。 我将通过键入以下内容来连接它:
$ dockerexec -it boilerplate_pg_1 /bin/bash
现在,当我进入pg容器时,我将运行psql client列出所有数据库:
$ psql -U postgres -W
根据.env
文件的密码只是一个“ 密码” 。 pg服务还使用.env
文件来创建样板数据库,而/ db / scripts文件夹中的自定义脚本负责创建样板测试数据库。 让我们确保一切都按计划进行。 输入\l
和肯定的事情我有boilerplate
和boilerplatetest
数据库准备与工作。
希望您学到了一些有用的东西。 在下一篇文章中,我将逐步创建实际的服务器,并提供一些使用中间件和处理程序的路由。 您还可以在这里看到整个项目。
翻译自: https://hackernoon.com/how-to-create-golang-rest-api-project-layout-configuration-part-1-am733yi7
golang api项目