~

Bootstrap Go

It is hard to write bootstrap tool to quickly create Go service. So I write this guide instead. This is a quick checklist for me every damn time I need to write a Go service from scratch. Also, this is my personal opinion, so feel free to comment.

Structure

main.go
internal/
    http/
        handler.go
        service.go
        models.go
    grpc/
        handler.go
        models.go
    consumer/
        handler.go
        service.go
        models.go
    service.go
    repository.go
    models.go

All codes are inside internal. Because internal is magic keyword in Go, you can not import pkg inside internal.

There are 3 common handlers:

For each handler, there are usually 3 layers: handler, service, repository:

Location:

Good taste

Stop using global var

If I see someone using global var, I swear I will shoot them twice in the face.

Why?

Avoid unsigned type

Just a var can not have negative value, doesn't mean it should use uint. Just use int and do not care about boundary.

Use functional options to init complex struct

func main() {
	s := NewS(WithA(1), WithB("b"))
	fmt.Printf("%+v\n", s)
}

type S struct {
	fieldA int
	fieldB string
}

type OptionS func(s *S)

func WithA(a int) OptionS {
	return func(s *S) {
		s.fieldA = a
	}
}

func WithB(b string) OptionS {
	return func(s *S) {
		s.fieldB = b
	}
}

func NewS(opts ...OptionS) *S {
	s := &S{}
	for _, opt := range opts {
		opt(s)
	}
	return s
}

In above example, I construct s with WithA and WithB option. No need to pass direct field inside s.

Use sync.WaitGroup, errgroup

If logic involves calling too many APIs, but they are not depend on each other. We can fire them parallel :)

Use errgroup if you need to cancel all tasks if first error is caught. Be super careful with egCtx, should use this instead of parent ctx inside eg.Go.

Example:

eg, egCtx := errgroup.WithContext(ctx)

eg.Go(func() error {
	// Do some thing
	return nil
})

eg.Go(func() error {
	// Do other thing
	return nil
})

if err := eg.Wait(); err != nil {
	// Handle error
}

Generics with some tricks

Take value then return pointer, useful with database struct full of pointers:

// Ptr takes in non-pointer and returns a pointer
func Ptr[T any](v T) *T {
	return &v
}

Return zero value:

func Zero[T any]() T {
  var zero T
  return zero
}

Modernize style

Sync go 1.26:

Sync go 1.24:

Sync go 1.22:

Since go 1.21:

Since go 1.20:

Since go 1.18:

Use gopls/modernize to modernize code.

modernize -fix -test ./...

External libs

No need go mod vendor

Save storage space.

Use bufbuild/buf for proto related

Please don't use grpc-ecosystem/grpc-gateway:

Use gin-gonic/gin for HTTP framework

With c *gin.Context:

Remember to free resources after parse multipart form:

defer func() {
    if err := c.Request.MultipartForm.RemoveAll(); err != nil {
        // Handle error
    }
}()

Combine with go-playground/validator to validate request structs.

Log with uber-go/zap

It is fast!

Read config with spf13/viper

Only init config in main or cmd layer. Do not use viper.Get... in inside layer.

Why?

Also, be careful if config value is empty. You should decide to continue or stop the service if there is empty config.

Connect database with go-gorm/gorm

Make sure to test your code (ORM or not) with DATA-DOG/go-sqlmock.

Connect Redis with redis/go-redis

Be careful when use HGETALL. If key not found, empty data will be returned not nil error. See redis/go-redis/issues/1668

Use Pipelines for:

Prefer to use Pipelined instead of Pipeline.

Example:

func (c *client) HSetWithExpire(ctx context.Context, key string, values []any, expired time.Duration) error {
	cmds := make([]redis.Cmder, 2)

	if _, err := c.Pipelined(ctx, func(pipe redis.Pipeliner) error {
		cmds[0] = pipe.HSet(ctx, key, values...)

		if expired > 0 {
			cmds[1] = pipe.Expire(ctx, key, expired)
		}

		return nil
	}); err != nil {
		return err
	}

	for _, cmd := range cmds {
		if cmd == nil {
			continue
		}

		if err := cmd.Err(); err != nil {
			return err
		}
	}

	return nil
}

Remember to config:

Connect MySQL with go-sql-driver/mysql

Remember to config:

Connect Kafka with IBM/sarama

Don't use confluentinc/confluent-kafka-go, because it's required CGO_ENABLED.

Test with stretchr/testify

It is easy to write a suite test, thanks to testify. Also, for mocking, there are many options out there. Pick 1 then sleep peacefully.

Mock with matryer/moq or uber/mock

The first is easy to use but not powerful as the later. If you want to make sure mock func is called with correct times, use the later.

Example with matryer/moq:

// Only gen mock if source code file is newer than mock file
// https://jonwillia.ms/2019/12/22/conditional-gomock-mockgen
//go:generate sh -c "test service_mock_generated.go -nt $GOFILE && exit 0; moq -rm -out service_mock_generated.go . Service"

Replace go fmt with mvdan/gofumpt

gofumpt provides more strict rules when format Go codes.

gofumpt -w -extra .

Use golangci/golangci-lint

No need to say more. Lint is the way!

My heuristic for fieldalignment (not work all the time): pointer -> string -> []byte -> int64 -> int32.

golangci-lint run --fix --no-config ./...

Scripts

Change import:

gofmt -w -r '"github.com/Sirupsen/logrus" -> "github.com/sirupsen/logrus"' *.go

Cleanup if storage is full:

go clean -cache -testcache -modcache -fuzzcache -x

Thanks


Source code is available on GitHub