0% found this document useful (0 votes)
17 views

Golang Project Best Practice

Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
17 views

Golang Project Best Practice

Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 94

Go Project Layout

and Practice
Bo-Yi Wu
2019.08.29
ModernWeb
About me
• Software Engineer in Mediatek

• Member of Drone CI/CD Platform

• Member of Gitea Platform

• Member of Gin Golang Framework

• Teacher of Udemy Platform: Golang + Drone


https://blog.wu-boy.com
Agenda
• Go in Mediatek

• Go Project Layout

• Go Practices

• RESTful api and GraphQL

• Model testing (Postgres, SQLite, MySQL)

• Software Quality

• Data Metrics

• Go Testing
Tech Stack

• Initial Project using Go in 2018/01

• Golang

• Easy to Learn

• Performance

• Deployment
├── api

Repository folder ├──



assets
└── dist
├── cmd
│ └── ggz
• api ├──
├──
configs
docker
│ ├── server
└── pkg
• assets ├── config
├── errors
├── fixtures
• cmd ├── helper
├── middleware
│ ├── auth
│ └── header
• configs ├── model
├── module
│ ├── mailer
• docker │

├── metrics
└── storage
├── router
│ └── routes
• pkg ├── schema
└── version
Root folder
• .drone.yml (deploy config)

• .revive.toml (golint config)

• docker-compose.yml (DB, Redis and UI)

• Makefile

• go module config (go.mod and go.sum)

• .env.example
Go Module
https://blog.golang.org/using-go-modules
Improve Deployment
Using Go Module Proxy
https://github.com/gomods/athens
save time
with proxy
97s -> 6s
Makefile
Build, Testing, Deploy
GOFMT ?= gofmt "-s"
GO ?= go
TARGETS ?= linux darwin windows
ARCHS ?= amd64 386
BUILD_DATE ?= $(shell date -u +"%Y-%m-%dT%H:%M:%SZ")
GOFILES := $(shell find . -name "*.go" -type f)
TAGS ?= sqlite sqlite_unlock_notify

ifneq ($(shell uname), Darwin)


EXTLDFLAGS = -extldflags "-static" $(null)
else
EXTLDFLAGS =
endif

ifneq ($(DRONE_TAG),)
VERSION ?= $(subst v,,$(DRONE_TAG))
else
VERSION ?= $(shell git describe --tags --always')
endif
.env
GGZ_DB_DRIVER=mysql
GGZ_DB_USERNAME=root
GGZ_DB_PASSWORD=123456
GGZ_DB_NAME=ggz
GGZ_DB_HOST=127.0.0.1:3307
GGZ_SERVER_ADDR=:8080
GGZ_DEBUG=true
GGZ_SERVER_HOST=http://localhost:8080
GGZ_STORAGE_DRIVER=disk
GGZ_MINIO_ACCESS_ID=xxxxxxxx
GGZ_MINIO_SECRET_KEY=xxxxxxxx
GGZ_MINIO_ENDPOINT=s3.example.com
GGZ_MINIO_BUCKET=example
GGZ_MINIO_SSL=true
GGZ_AUTH0_DEBUG=true
docker-compose.yml
db:
image: mysql
restart: always
Development
volumes:
- mysql-data:/var/lib/mysql
environment:
MYSQL_USER: example
MYSQL_PASSWORD: example
MYSQL_DATABASE: example
MYSQL_ROOT_PASSWORD: example

minio:
image: minio/minio
restart: always
ports:
volumes:
- minio-data:/data
environment:
MINIO_ACCESS_KEY: minio123456
MINIO_SECRET_KEY: minio123456
command: server /data
api: Production
image: foo/bar
restart: always
ports:
- 8080:8080
environment:
- GGZ_METRICS_TOKEN=test-prometheus-token
- GGZ_METRICS_ENABLED=true
labels:
- "traefik.enable=true"
- "traefik.basic.frontend.rule=Host:${WEB_HOST}"
- "traefik.basic.protocol=http"
Version
Compile version info into Go binary
Version

• -X github.com/go-ggz/ggz/pkg/
version.Version=$(VERSION)

• -X github.com/go-ggz/ggz/pkg/
version.BuildDate=$(BUILD_DATE)

go build -o bin/api -ldflags


var (
// Version number for git tag.
Version string
// BuildDate is the ISO 8601 day drone was built.
BuildDate string
)

// PrintCLIVersion print server info


func PrintCLIVersion() string {
return fmt.Sprintf(
"version %s, built on %s, %s",
Version,
BuildDate,
runtime.Version(),
)
}
BUILD_DATE ?= $(shell date -u +"%Y-%m-%dT%H:%M:%SZ")
ifneq ($(DRONE_TAG),)
VERSION ?= $(subst v,,$(DRONE_TAG))
else
VERSION ?= $(shell git describe --tags --always | sed 's/-/+/' | sed
's/^v//')
endif
Assets
Embed files in Go

https://github.com/UnnoTed/fileb0x
func ReadSource(origPath string) (content []byte, err error) {
content, err = ReadFile(origPath)

if err != nil {
log.Warn().Err(err).Msgf("Failed to read builtin %s file.", origPath)
}
Debug Setting
if config.Server.Assets != "" && file.IsDir(config.Server.Assets) {
origPath = path.Join(config.Server.Assets, origPath)

if file.IsFile(origPath) {
content, err = ioutil.ReadFile(origPath)
if err != nil {
log.Warn().Err(err).Msgf("Failed to read custom %s file", origPath)
}
}
}

return content, err


}
// ViewHandler support dist handler from UI
func ViewHandler() gin.HandlerFunc {
fileServer := http.FileServer(dist.HTTP)
data := []byte(time.Now().String())
etag := fmt.Sprintf("%x", md5.Sum(data))

return func(c *gin.Context) {


c.Header("Cache-Control", "public, max-age=31536000")
c.Header("ETag", etag)

if match := c.GetHeader("If-None-Match"); match != "" {


if strings.Contains(match, etag) {
c.Status(http.StatusNotModified)
return
}
}
File Server Handler

fileServer.ServeHTTP(c.Writer, c.Request)
}
}
// Favicon represents the favicon.
func Favicon(c *gin.Context) {
file, _ := dist.ReadFile("favicon.ico")
etag := fmt.Sprintf("%x", md5.Sum(file))
c.Header("ETag", etag)
c.Header("Cache-Control", "max-age=0")NO Cache

if match := c.GetHeader("If-None-Match"); match != "" {


if strings.Contains(match, etag) {
c.Status(http.StatusNotModified)
return
}
}

c.Data(
http.StatusOK,
"image/x-icon",
file,
)
}
API
/healthz
• health check for load balancer

func Heartbeat(c *gin.Context) {


c.AbortWithStatus(http.StatusOK)
c.String(http.StatusOK, "ok")
}
CMD
Command line
Command line package

• Golang package: flag

• urfave/cli

• spf13/cobra
├── agent
│ ├── config
│ │ └── config.go
│ └── main.go
├── notify
│ └── main.go
└── tcp-server
├── config
│ └── config.go
└── main.go
Config
Management
github.com/spf13/viper
Config management

• Load config from File

• .json

• .ini

• Load config from Environment Variables

• .env
var envfile string
flag.StringVar(&envfile, "env-file", ".env", "Read in a file of environment
variables")
flag.Parse()

godotenv.Load(envfile)

_ "github.com/joho/godotenv/autoload"
Logging struct {
Debug bool `envconfig:"GGZ_LOGS_DEBUG"`
Level string `envconfig:"GGZ_LOGS_LEVEL" default:"info"`
Color bool `envconfig:"GGZ_LOGS_COLOR"`
Pretty bool `envconfig:"GGZ_LOGS_PRETTY"`
Text bool `envconfig:"GGZ_LOGS_TEXT"`
}

// Server provides the server configuration.


Server struct {
Addr string `envconfig:"GGZ_SERVER_ADDR"`
Port string `envconfig:"GGZ_SERVER_PORT" default:"12000"`
Path string `envconfig:”GGZ_SERVER_PATH" default:"data"`
}
github.com/kelseyhightower/envconfig
config, err := config.Environ()
if err != nil {
log.Fatal().
Err(err).
Msg("invalid configuration")
}

initLogging(config)

// check folder exist Load env from structure


if !file.IsDir(config.Server.Path) {
log.Fatal().
Str("path", config.Server.Path).
Msg("log folder not found")
}
/configs
Configuration file templates or default config
global:
scrape_interval: 5s
external_labels:
monitor: 'my-monitor'
scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
- job_name: 'ggz-server'
static_configs:
- targets: ['ggz-server:8080']
bearer_token: 'test-prometheus-token'
/docker
Docker file template
├── ggz-redirect
│ ├── Dockerfile.linux.amd64
│ ├── Dockerfile.linux.arm
│ ├── Dockerfile.linux.arm64
│ ├── Dockerfile.windows.amd64
│ └── manifest.tmpl
└── ggz-server
├── Dockerfile.linux.amd64
├── Dockerfile.linux.arm
├── Dockerfile.linux.arm64
├── Dockerfile.windows.amd64
└── manifest.tmpl
/integrations
ctx := context.Background()
req := testcontainers.ContainerRequest{
Image: "goggz/ggz-server",
ExposedPorts: []string{"8080/tcp"},
WaitingFor: wait.ForLog("Starting shorten server on :8080")
}
ggzServer, err := testcontainers.GenericContainer(
ctx,
testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})

if err != nil {
t.Fatal(err)
}
github.com/testcontainers/testcontainers-go
/pkg
├── config
├── errors
├── fixtures
├── helper
├── middleware
│ ├── auth
│ └── header
├── model
├── module
│ ├── metrics
│ └── storage
│ ├── disk
│ └── minio
├── router
│ └── routes
├── schema
└── version
/pkg/errors
// Type defines the type of an error
type Type string

const (
// Internal error
Internal Type = "internal"
// NotFound error means that a specific item does not exis
NotFound Type = "not_found"
// BadRequest error
BadRequest Type = "bad_request"
// Validation error
Validation Type = "validation"
// AlreadyExists error
AlreadyExists Type = "already_exists"
// Unauthorized error
Unauthorized Type = "unauthorized"
)
// ENotExists creates an error of type NotExist
func ENotExists(msg string, err error, arg ...interface{}) error {
return New(NotFound, fmt.Sprintf(msg, arg...), err)
}

// EBadRequest creates an error of type BadRequest


func EBadRequest(msg string, err error, arg ...interface{}) error {
return New(BadRequest, fmt.Sprintf(msg, arg...), err)
}

// EAlreadyExists creates an error of type AlreadyExists


func EAlreadyExists(msg string, err error, arg ...interface{}) error {
return New(AlreadyExists, fmt.Sprintf(msg, arg...), err)
}
/pkg/fixtures
Rails-like test fixtures
Write tests against a real database
github.com/go-testfixtures/testfixtures
fixtures/
posts.yml
comments.yml
tags.yml
posts_tags.yml
users.yml
-
id: 1
email: [email protected]
full_name: test
avatar: http://example.com
avatar_email: [email protected]

-
id: 2
email: [email protected]
full_name: test1234
avatar: http://example.com
avatar_email: [email protected]
Unit Testing with Database
func TestMain(m *testing.M) {
// test program to do extra
setup or teardown before or after
testing.
os.Exit(m.Run())
}

https://golang.org/pkg/testing/#hdr-Main
func MainTest(m *testing.M, pathToRoot string) {
var err error
fixturesDir := filepath.Join(pathToRoot, "pkg", "fixtures")
if err = createTestEngine(fixturesDir); err != nil {
fatalTestError("Error creating test engine: %v\n", err)
}
os.Exit(m.Run())
}

func createTestEngine(fixturesDir string) error {


var err error
x, err = xorm.NewEngine("sqlite3", "file::memory:?cache=shared")
if err != nil {
return err
Testing with SQLite
}

x.ShowSQL(config.Server.Debug)

return InitFixtures(&testfixtures.SQLite{}, fixturesDir)


}
func TestIsUserExist(t *testing.T) {
assert.NoError(t, PrepareTestDatabase())
exists, err := IsUserExist(0, "[email protected]")
assert.NoError(t, err)
assert.True(t, exists)

exists, err = IsUserExist(0, "[email protected]")


assert.NoError(t, err)
assert.False(t, exists)

exists, err = IsUserExist(1, "[email protected]")


assert.NoError(t, err)
assert.True(t, exists)

exists, err = IsUserExist(1, "[email protected]")


assert.NoError(t, err)
assert.False(t, exists)
} go test -v -run=TestIsUserExist ./pkg/models/
/pkg/helper
Helper func

• Encrypt and Decrypt

• Regexp func

• IsEmail, IsUsername

• Zipfile
/pkg/middleware
func Secure(c *gin.Context) {
c.Header("Access-Control-Allow-Origin", "*")
c.Header("X-Frame-Options", "DENY")
c.Header("X-Content-Type-Options", "nosniff")
c.Header("X-XSS-Protection", "1; mode=block")
if c.Request.TLS != nil {
c.Header("Strict-Transport-Security", "max-age=31536000")
}
}
/pkg/model
Use gorm or xorm
Build in
SQLite3
// +build sqlite
go build -v -tags 'sqlite sqlite_unlock_notify'

package model

import (
_ "github.com/mattn/go-sqlite3"
)

func init() {
EnableSQLite3 = true
}
/pkg/module
├── cron
├── download
├── jwt
├── ldap
├── mailer
│ ├── ses
│ └── smtp
├── queue
├── metrics
├── redis
└── storage
├── disk
└── minio
Integration with
Prometheus + Grafana
func NewCollector() Collector {
return Collector{
Users: prometheus.NewDesc(
namespace+"users",
"Number of Users",
nil, nil,
),
}

// Collect returns the metrics with values


func (c Collector) Collect(ch chan<- prometheus.Metric) {
stats := model.GetStatistic()

ch <- prometheus.MustNewConstMetric(
c.Users,
prometheus.GaugeValue,
float64(stats.Counter.User),
)
}
Prometheus Handler
func Metrics(token string) gin.HandlerFunc {
h := promhttp.Handler()
return func(c *gin.Context) {
if token == "" {
h.ServeHTTP(c.Writer, c.Request)
return
}
header := c.Request.Header.Get("Authorization")
if header == "" {
c.String(http.StatusUnauthorized, errInvalidToken.Error())
return
}
bearer := fmt.Sprintf("Bearer %s", token)
if header != bearer {
c.String(http.StatusUnauthorized, errInvalidToken.Error())
return
}
h.ServeHTTP(c.Writer, c.Request)
}
}
c := metrics.NewCollector()
prometheus.MustRegister(c)
if config.Metrics.Enabled {
root.GET("/metrics", router.Metrics(config.Metrics.Token))
}
Your prometheus token
/pkg/schema
RESTful vs GraphQL
See the Slide: GraphQL in Go
var rootQuery = graphql.NewObject(
graphql.ObjectConfig{
Name: "RootQuery",
Description: "Root Query",
Fields: graphql.Fields{
"queryShortenURL": &queryShortenURL,
"queryMe": &queryMe,
},
})

var rootMutation = graphql.NewObject(


graphql.ObjectConfig{
Name: "RootMutation",
Description: "Root Mutation",
Fields: graphql.Fields{
"createUser": &createUser,
},
})

// Schema is the GraphQL schema served by the server.


var Schema, _ = graphql.NewSchema(
graphql.SchemaConfig{
Query: rootQuery,
Mutation: rootMutation,
})
Write the GraphQL Testing
assert.NoError(t, model.PrepareTestDatabase())
t.Run("user not login", func(t *testing.T) {
test := T{
Query: `{
queryMe {
email
}
}`,
Schema: Schema,
Expected: &graphql.Result{
Data: map[string]interface{}{
"queryMe": nil,
},
Errors: []gqlerrors.FormattedError{
{
Message: errorYouAreNotLogin,
},
},
},
}
})
}
Best Practice
Testing your Go code
Testable Code

• Code Quality

• Readability

• Maintainability

• Testability
#1. Testing in Go

go test package_name

func TestFooBar(t *testing.T) {}


func ExampleFooBar(t *testing.T) {}
func BenchmarkFooBar(b *testing.B) {}
#2. Benchmark Testing
Profiling: CPU, Memory, Goroutine Block
func BenchmarkPlaylyfeGraphQLMaster(b *testing.B) {
for i := 0; i < b.N; i++ {
context := map[string]interface{}{}
variables := map[string]interface{}{}
playlyfeExecutor.Execute(context, "{hello}", variables, "")
}
}

func BenchmarkGophersGraphQLMaster(b *testing.B) {


for i := 0; i < b.N; i++ {
ctx := context.Background()
variables := map[string]interface{}{}
gopherSchema.Exec(ctx, "{hello}", "", variables)
}
} http://bit.ly/2L0CG3Q
#3. Example Testing
Examples on how to use your code
func ExampleFooBar() {
fmt.Println(strings.Compare("a", "b"))
fmt.Println(strings.Compare("a", "a"))
fmt.Println(strings.Compare("b", "a"))
// Output:
// -1
// 0
// 1
}
$ go test -v -tags=sqlite -run=ExampleFooBar ./pkg/model/...
=== RUN ExampleFooBar
--- PASS: ExampleFooBar (0.00s)
PASS
ok github.com/go-ggz/ggz/pkg/model 0.022s
#4. Subtests in Testing Package

func (t *T) Run(name string, f func(t *T)) bool {}


func (b *B) Run(name string, f func(b *B)) bool {}
tests := []struct {
name string
fields fields
args args
}{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := Collector{
Shortens: tt.fields.Shortens,
Users: tt.fields.Users,
}
c.Describe(tt.args.ch)
})
}
}
#5. Skipping Testing
t.Skip()
package metrics

import (
"os"
"testing"
)

func TestSkip(t *testing.T) {


if os.Getenv("DEBUG_MODE") == "true" {
t.Skipf("test skipped")
}
}
#6. Running Tests in Parallel
Speedup your CI/CD Flow
t.Parallel()
func TestFooBar01(t *testing.T) {
t.Parallel()
time.Sleep(time.Second)
}
func TestFooBar02(t *testing.T) {
t.Parallel()
time.Sleep(time.Second * 2)
}
func TestFooBar03(t *testing.T) {
t.Parallel()
time.Sleep(time.Second * 3)
}
Just only use
one package
github.com/stretchr/testify
https://www.udemy.com/course/golang-fight/?couponCode=GOLANG2019
https://www.udemy.com/course/devops-oneday/?couponCode=DRONE2019
END

You might also like