Golang Project Best Practice
Golang Project Best Practice
and Practice
Bo-Yi Wu
2019.08.29
ModernWeb
About me
• Software Engineer in Mediatek
• Go Project Layout
• Go Practices
• Software Quality
• Data Metrics
• Go Testing
Tech Stack
• Golang
• Easy to Learn
• Performance
• Deployment
├── api
• Makefile
• .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 ($(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)
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)
}
}
}
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
c.Data(
http.StatusOK,
"image/x-icon",
file,
)
}
API
/healthz
• health check for load balancer
• 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
• .json
• .ini
• .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"`
}
initLogging(config)
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)
}
-
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())
}
x.ShowSQL(config.Server.Debug)
• 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,
),
}
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,
},
})
• Code Quality
• Readability
• Maintainability
• Testability
#1. Testing in Go
go test package_name
import (
"os"
"testing"
)