如何创建Golang REST API:项目布局配置[第1部分]

在过去的几年中,我从事过数个用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 container
COPY  . .

# This container 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 container ls

我可以在名称列中分配pg servide名称。 就我而言,码头工人已将其命名为boilerplate_pg_1。 我将通过键入以下内容来连接它:

$ docker exec -it boilerplate_pg_1 /bin/bash

现在,当我进入pg容器时,我将运行psql client列出所有数据库:

$ psql -U postgres -W

根据.env文件的密码只是一个“ 密码” 。 pg服务还使用.env文件来创建样板数据库,而/ db / scripts文件夹中的自定义脚本负责创建样板测试数据库。 让我们确保一切都按计划进行。 输入\l

和肯定的事情我有boilerplateboilerplatetest数据库准备与工作。

希望您学到了一些有用的东西。 在下一篇文章中,我将逐步创建实际的服务器,并提供一些使用中间件和处理程序的路由。 您还可以在这里看到整个项目。

From: https://hackernoon.com/how-to-create-golang-rest-api-project-layout-configuration-part-1-am733yi7

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值