SlideShare a Scribd company logo
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
Repository folder
• api
• assets
• cmd
• configs
• docker
• pkg
├── api
├── assets
│   └── dist
├── cmd
│   └── ggz
├── configs
├── docker
│   ├── server
└── pkg
├── config
├── errors
├── fixtures
├── helper
├── middleware
│   ├── auth
│   └── header
├── model
├── module
│   ├── mailer
│   ├── metrics
│   └── storage
├── router
│   └── routes
├── 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
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
Development
Productionapi:
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"
VersionCompile 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
AssetsEmbed 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)
  }
  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
}
Debug Setting
// 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
      }
    }
    fileServer.ServeHTTP(c.Writer, c.Request)
  }
}
File Server Handler
圖片來來源:https://developers.google.com/web/fundamentals/performance/optimizing-
content-efficiency/http-caching?hl=zh-tw
// 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")
  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,
  )
}
NO Cache
API
/healthz
• health check for load balancer
func Heartbeat(c *gin.Context) {
  c.AbortWithStatus(http.StatusOK)
  c.String(http.StatusOK, "ok")
}
CMDCommand 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
  if !file.IsDir(config.Server.Path) {
    log.Fatal().
      Str("path", config.Server.Path).
      Msg("log folder not found")
  }
Load env from structure
/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: test@gmail.com
full_name: test
avatar: http://example.com
avatar_email: test@gmail.com
-
id: 2
email: test1234@gmail.com
full_name: test1234
avatar: http://example.com
avatar_email: test1234@gmail.com
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: %vn", 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
  }
  x.ShowSQL(config.Server.Debug)
  return InitFixtures(&testfixtures.SQLite{}, fixturesDir)
}
Testing with SQLite
func TestIsUserExist(t *testing.T) {
  assert.NoError(t, PrepareTestDatabase())
  exists, err := IsUserExist(0, "test@gmail.com")
  assert.NoError(t, err)
  assert.True(t, exists)
  exists, err = IsUserExist(0, "test123456@gmail.com")
  assert.NoError(t, err)
  assert.False(t, exists)
  exists, err = IsUserExist(1, "test1234@gmail.com")
  assert.NoError(t, err)
  assert.True(t, exists)
  exists, err = IsUserExist(1, "test123456@gmail.com")
  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
package model
import (
  _ "github.com/mattn/go-sqlite3"
)
func init() {
  EnableSQLite3 = true
}
go build -v -tags 'sqlite sqlite_unlock_notify'
/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
Golang Project Layout and Practice
Testable Code
• Code Quality
• Readability
• Maintainability
• Testability
#1. Testing in Go
func TestFooBar(t *testing.T) {}
func ExampleFooBar(t *testing.T) {}
func BenchmarkFooBar(b *testing.B) {}
go test package_name
#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
Golang Project Layout and Practice
#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

More Related Content

PPTX
BDD with SpecFlow and Selenium
PPTX
Clean code
PDF
Multiplayer Game Sync Techniques through CAP theorem
PDF
Clean code
PPTX
[Webinar] Qt Test-Driven Development Using Google Test and Google Mock
 
PPTX
유니티 + Nodejs를 활용한 멀티플레이어 게임 개발하기
PPTX
Clean code
PDF
jQuery -Chapter 2 - Selectors and Events
BDD with SpecFlow and Selenium
Clean code
Multiplayer Game Sync Techniques through CAP theorem
Clean code
[Webinar] Qt Test-Driven Development Using Google Test and Google Mock
 
유니티 + Nodejs를 활용한 멀티플레이어 게임 개발하기
Clean code
jQuery -Chapter 2 - Selectors and Events

What's hot (20)

PDF
PDF
송창규, unity build로 빌드타임 반토막내기, NDC2010
PDF
Devops Porto - CI/CD at Gitlab
PPTX
Introduction to java
PDF
NDC 2015 삼시세끼 빌드만들기
PDF
An Introduction to JUnit 5 and how to use it with Spring boot tests and Mockito
PDF
Java Garbage Collection - How it works
PPTX
Git Pull Requests
PPTX
Getting started with typescript
PDF
Object-oriented Programming-with C#
PDF
Git - An Introduction
PDF
테라로 살펴본 MMORPG의 논타겟팅 시스템
PDF
오딘: 발할라 라이징 MMORPG의 성능 최적화 사례 공유 [카카오게임즈 - 레벨 300] - 발표자: 김문권, 팀장, 라이온하트 스튜디오...
ODP
Qt Workshop
PDF
Learning git
PDF
Introduction to TypeScript
PPS
JUnit Presentation
PDF
katalon studio 툴을 이용한 GUI 테스트 자동화 가이드
PPTX
Java version 11 - les 9 nouveautes
PDF
Memory & object pooling
송창규, unity build로 빌드타임 반토막내기, NDC2010
Devops Porto - CI/CD at Gitlab
Introduction to java
NDC 2015 삼시세끼 빌드만들기
An Introduction to JUnit 5 and how to use it with Spring boot tests and Mockito
Java Garbage Collection - How it works
Git Pull Requests
Getting started with typescript
Object-oriented Programming-with C#
Git - An Introduction
테라로 살펴본 MMORPG의 논타겟팅 시스템
오딘: 발할라 라이징 MMORPG의 성능 최적화 사례 공유 [카카오게임즈 - 레벨 300] - 발표자: 김문권, 팀장, 라이온하트 스튜디오...
Qt Workshop
Learning git
Introduction to TypeScript
JUnit Presentation
katalon studio 툴을 이용한 GUI 테스트 자동화 가이드
Java version 11 - les 9 nouveautes
Memory & object pooling
Ad

Similar to Golang Project Layout and Practice (20)

PDF
[EXTENDED] Ceph, Docker, Heroku Slugs, CoreOS and Deis Overview
PDF
Let Grunt do the work, focus on the fun!
PDF
Delivering Go.CD with Terraform and Docker
PDF
Into The Box 2018 Going live with commandbox and docker
PDF
Going live with BommandBox and docker Into The Box 2018
PDF
如何透過 Go-kit 快速搭建微服務架構應用程式實戰
PDF
Go 1.10 Release Party - PDX Go
PDF
Bangpypers april-meetup-2012
PDF
SF Grails - Ratpack - Compact Groovy Webapps - James Williams
PDF
Google App Engine: Basic
PDF
I Just Want to Run My Code: Waypoint, Nomad, and Other Things
PDF
The Fairy Tale of the One Command Build Script
PPTX
FP - Découverte de Play Framework Scala
PDF
Developing with-devstack
PPTX
betterCode Workshop: Effizientes DevOps-Tooling mit Go
PDF
Dev ninja -> vagrant + virtualbox + chef-solo + git + ec2
PPTX
Toolbox of a Ruby Team
PPTX
Настройка окружения для кросскомпиляции проектов на основе docker'a
PDF
Let Grunt do the work, focus on the fun! [Open Web Camp 2013]
PDF
GDG Cloud Taipei meetup #50 - Build go kit microservices at kubernetes with ...
[EXTENDED] Ceph, Docker, Heroku Slugs, CoreOS and Deis Overview
Let Grunt do the work, focus on the fun!
Delivering Go.CD with Terraform and Docker
Into The Box 2018 Going live with commandbox and docker
Going live with BommandBox and docker Into The Box 2018
如何透過 Go-kit 快速搭建微服務架構應用程式實戰
Go 1.10 Release Party - PDX Go
Bangpypers april-meetup-2012
SF Grails - Ratpack - Compact Groovy Webapps - James Williams
Google App Engine: Basic
I Just Want to Run My Code: Waypoint, Nomad, and Other Things
The Fairy Tale of the One Command Build Script
FP - Découverte de Play Framework Scala
Developing with-devstack
betterCode Workshop: Effizientes DevOps-Tooling mit Go
Dev ninja -> vagrant + virtualbox + chef-solo + git + ec2
Toolbox of a Ruby Team
Настройка окружения для кросскомпиляции проектов на основе docker'a
Let Grunt do the work, focus on the fun! [Open Web Camp 2013]
GDG Cloud Taipei meetup #50 - Build go kit microservices at kubernetes with ...
Ad

More from Bo-Yi Wu (20)

PDF
Drone CI/CD 自動化測試及部署
PDF
用 Go 語言打造多台機器 Scale 架構
PDF
Job Queue in Golang
PDF
Introduction to GitHub Actions
PDF
Drone 1.0 Feature
PDF
Drone CI/CD Platform
PDF
GraphQL IN Golang
PPTX
Go 語言基礎簡介
PPTX
drone continuous Integration
PPTX
Gorush: A push notification server written in Go
PPTX
用 Drone 打造 輕量級容器持續交付平台
PPTX
用 Go 語言 打造微服務架構
PPTX
Introduction to Gitea with Drone
PDF
運用 Docker 整合 Laravel 提升團隊開發效率
PDF
用 Go 語言實戰 Push Notification 服務
PPTX
用 Go 語言打造 DevOps Bot
PPTX
A painless self-hosted Git service: Gitea
PPTX
Write microservice in golang
PPTX
用 Docker 改善團隊合作模式
PPTX
Git flow 與團隊合作
Drone CI/CD 自動化測試及部署
用 Go 語言打造多台機器 Scale 架構
Job Queue in Golang
Introduction to GitHub Actions
Drone 1.0 Feature
Drone CI/CD Platform
GraphQL IN Golang
Go 語言基礎簡介
drone continuous Integration
Gorush: A push notification server written in Go
用 Drone 打造 輕量級容器持續交付平台
用 Go 語言 打造微服務架構
Introduction to Gitea with Drone
運用 Docker 整合 Laravel 提升團隊開發效率
用 Go 語言實戰 Push Notification 服務
用 Go 語言打造 DevOps Bot
A painless self-hosted Git service: Gitea
Write microservice in golang
用 Docker 改善團隊合作模式
Git flow 與團隊合作

Recently uploaded (20)

PDF
Advanced Soft Computing BINUS July 2025.pdf
PPT
Teaching material agriculture food technology
PDF
Sensors and Actuators in IoT Systems using pdf
PDF
Per capita expenditure prediction using model stacking based on satellite ima...
PDF
Advanced IT Governance
PDF
CIFDAQ's Market Insight: SEC Turns Pro Crypto
PPT
“AI and Expert System Decision Support & Business Intelligence Systems”
PPTX
breach-and-attack-simulation-cybersecurity-india-chennai-defenderrabbit-2025....
PPTX
Detection-First SIEM: Rule Types, Dashboards, and Threat-Informed Strategy
PDF
Spectral efficient network and resource selection model in 5G networks
PDF
Electronic commerce courselecture one. Pdf
PDF
madgavkar20181017ppt McKinsey Presentation.pdf
PDF
HCSP-Presales-Campus Network Planning and Design V1.0 Training Material-Witho...
PDF
Chapter 2 Digital Image Fundamentals.pdf
PDF
solutions_manual_-_materials___processing_in_manufacturing__demargo_.pdf
PPTX
PA Analog/Digital System: The Backbone of Modern Surveillance and Communication
PDF
The Rise and Fall of 3GPP – Time for a Sabbatical?
PDF
CIFDAQ's Market Wrap: Ethereum Leads, Bitcoin Lags, Institutions Shift
PDF
Chapter 3 Spatial Domain Image Processing.pdf
PPTX
Telecom Fraud Prevention Guide | Hyperlink InfoSystem
Advanced Soft Computing BINUS July 2025.pdf
Teaching material agriculture food technology
Sensors and Actuators in IoT Systems using pdf
Per capita expenditure prediction using model stacking based on satellite ima...
Advanced IT Governance
CIFDAQ's Market Insight: SEC Turns Pro Crypto
“AI and Expert System Decision Support & Business Intelligence Systems”
breach-and-attack-simulation-cybersecurity-india-chennai-defenderrabbit-2025....
Detection-First SIEM: Rule Types, Dashboards, and Threat-Informed Strategy
Spectral efficient network and resource selection model in 5G networks
Electronic commerce courselecture one. Pdf
madgavkar20181017ppt McKinsey Presentation.pdf
HCSP-Presales-Campus Network Planning and Design V1.0 Training Material-Witho...
Chapter 2 Digital Image Fundamentals.pdf
solutions_manual_-_materials___processing_in_manufacturing__demargo_.pdf
PA Analog/Digital System: The Backbone of Modern Surveillance and Communication
The Rise and Fall of 3GPP – Time for a Sabbatical?
CIFDAQ's Market Wrap: Ethereum Leads, Bitcoin Lags, Institutions Shift
Chapter 3 Spatial Domain Image Processing.pdf
Telecom Fraud Prevention Guide | Hyperlink InfoSystem

Golang Project Layout and Practice

  • 1. Go Project Layout and Practice Bo-Yi Wu 2019.08.29 ModernWeb
  • 2. 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
  • 3. Agenda • Go in Mediatek • Go Project Layout • Go Practices • RESTful api and GraphQL • Model testing (Postgres, SQLite, MySQL) • Software Quality • Data Metrics • Go Testing
  • 4. Tech Stack • Initial Project using Go in 2018/01 • Golang • Easy to Learn • Performance • Deployment
  • 5. Repository folder • api • assets • cmd • configs • docker • pkg ├── api ├── assets │   └── dist ├── cmd │   └── ggz ├── configs ├── docker │   ├── server └── pkg ├── config ├── errors ├── fixtures ├── helper ├── middleware │   ├── auth │   └── header ├── model ├── module │   ├── mailer │   ├── metrics │   └── storage ├── router │   └── routes ├── schema └── version
  • 6. 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
  • 8. Improve Deployment Using Go Module Proxy https://github.com/gomods/athens
  • 11. 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
  • 12. .env
  • 15. db: image: mysql restart: always 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 Development
  • 16. Productionapi: 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"
  • 17. VersionCompile version info into Go binary
  • 18. 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
  • 19. 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(),   ) }
  • 20. 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
  • 21. AssetsEmbed files in Go https://github.com/UnnoTed/fileb0x
  • 22. 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)   }   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 } Debug Setting
  • 23. // 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       }     }     fileServer.ServeHTTP(c.Writer, c.Request)   } } File Server Handler
  • 25. // 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")   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,   ) } NO Cache
  • 26. API
  • 27. /healthz • health check for load balancer func Heartbeat(c *gin.Context) {   c.AbortWithStatus(http.StatusOK)   c.String(http.StatusOK, "ok") }
  • 29. Command line package • Golang package: flag • urfave/cli • spf13/cobra
  • 30. ├── agent │   ├── config │   │   └── config.go │   └── main.go ├── notify │   └── main.go └── tcp-server ├── config │   └── config.go └── main.go
  • 32. Config management • Load config from File • .json • .ini • Load config from Environment Variables • .env
  • 33.   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"
  • 34.   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
  • 35.   config, err := config.Environ()   if err != nil {     log.Fatal().       Err(err).       Msg("invalid configuration")   }   initLogging(config)   // check folder exist   if !file.IsDir(config.Server.Path) {     log.Fatal().       Str("path", config.Server.Path).       Msg("log folder not found")   } Load env from structure
  • 37. 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'
  • 39. ├── 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
  • 41.   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
  • 42. /pkg
  • 43. ├── config ├── errors ├── fixtures ├── helper ├── middleware │ ├── auth │ └── header ├── model ├── module │ ├── metrics │ └── storage │ ├── disk │ └── minio ├── router │ └── routes ├── schema └── version
  • 45. // 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" )
  • 46. // 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) }
  • 48. Rails-like test fixtures Write tests against a real database github.com/go-testfixtures/testfixtures
  • 50. - 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]
  • 51. Unit Testing with Database
  • 52. 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
  • 53. 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: %vn", 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   }   x.ShowSQL(config.Server.Debug)   return InitFixtures(&testfixtures.SQLite{}, fixturesDir) } Testing with SQLite
  • 54. 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/
  • 56. Helper func • Encrypt and Decrypt • Regexp func • IsEmail, IsUsername • Zipfile
  • 58. 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")   } }
  • 60. Use gorm or xorm
  • 62. // +build sqlite package model import (   _ "github.com/mattn/go-sqlite3" ) func init() {   EnableSQLite3 = true } go build -v -tags 'sqlite sqlite_unlock_notify'
  • 64. ├── cron ├── download ├── jwt ├── ldap ├── mailer │ ├── ses │ └── smtp ├── queue ├── metrics ├── redis └── storage ├── disk └── minio
  • 66. 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),   ) }
  • 68. 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)   } }
  • 69.     c := metrics.NewCollector()     prometheus.MustRegister(c)     if config.Metrics.Enabled {       root.GET("/metrics", router.Metrics(config.Metrics.Token))     } Your prometheus token
  • 71. RESTful vs GraphQL See the Slide: GraphQL in Go
  • 72. 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,   })
  • 73. Write the GraphQL Testing
  • 74.   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,           },         },       },     }   }) }
  • 77. Testable Code • Code Quality • Readability • Maintainability • Testability
  • 78. #1. Testing in Go func TestFooBar(t *testing.T) {} func ExampleFooBar(t *testing.T) {} func BenchmarkFooBar(b *testing.B) {} go test package_name
  • 79. #2. Benchmark Testing Profiling: CPU, Memory, Goroutine Block
  • 80. 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
  • 82. #3. Example Testing Examples on how to use your code
  • 83. func ExampleFooBar() {   fmt.Println(strings.Compare("a", "b"))   fmt.Println(strings.Compare("a", "a"))   fmt.Println(strings.Compare("b", "a"))   // Output:   // -1   // 0   // 1 }
  • 84. $ 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
  • 85. #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 {}
  • 86.   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)     })   } }
  • 88. package metrics import (   "os"   "testing" ) func TestSkip(t *testing.T) {   if os.Getenv("DEBUG_MODE") == "true" {     t.Skipf("test skipped")   } }
  • 89. #6. Running Tests in Parallel Speedup your CI/CD Flow t.Parallel()
  • 90. 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) }
  • 91. Just only use one package github.com/stretchr/testify
  • 94. END