Functionality
Untility
Non-Complexity
This is a template for creating a Go web service API project. Intended to be used as a starting point for creating a new Go web service API project and be a guideline for the project structure.
- Standardized Go project structure: Offer a pre-configured project structure, essential tools, and best practices to reduce setup time and accelerate development.
- Facilitate quality: Enforce coding standards, Provide a testing framework and examples to encourage thorough testing and maintain code quality such as golangci-lint, govulncheck, and pre-commit etc.
- Enhance security: Implement security measures from the start, such as vulnerability scanning with govulncheck and secure coding practices.
- Support best practices: Adhere to industry-standard desig 8000 n patterns and architectural principles for Go web services.
- Be extensible: Allow for easy customization and integration with various tool and frameworks.
NOTE you can DELETE anything that you DON'T NEED in this template.
- Pre-requisite: before getting started or git clone this repository
- How to copy this template
- Project structural components
- Guideline How To Code
- Conventions
- When to use log.Fatal/log.Panic
- Gracefully shutting down
- Libraies suggestion
- Bad Practices
- Contribution-Guide
- References
- Cross Functional Requirements
- Technical Requirement Guidelines
see contents
- Go 1.23 or later https://go.dev/dl/
- set up GOPRIVATE environment variable to allow go get private repository
go env -w GOPRIVATE=xxx.com
- config ssh for private repo GitLab SSH Keys. otherwise, you will not be able to install use
gonew
command to create a new project.- or use personal access token set in
~/.netrc
file
- or use personal access token set in
if you are using ssh then you can add the following configuration to your git config file.
- install
gonew
to create a new projectgo install golang.org/x/tools/cmd/gonew@latest
- install direnv to manage environment variables direnv
brew install direnv
echo "eval \"$(direnv hook bash)\"" >> ~/.zshrc
if you are using zsh. otherwise, use~/.bashrc
source ~/.zshrc
- config direnv look at
.env
file by config~/.config/direnv/direnv.toml
[global]
load_dotenv = true
see contents
you also can just copy this repository and rename it to your project name and rename or the import module name to your project name.
However, you can use gonew
command to archive the same result.
- make sure you set
go env -w GOPRIVATE=xxx.com
(to allow go get private repository) - copy this repository and rename it to your project name or use
gonew github.com/pallat/wtf github.com/pallat/api
- cd to your project directory
cd xxxx
the - make your project under version control by running
git init
- and run
make setup
to install necessary tools - test the installation by running
make test
to run all the tests - load environment variables by running
direnv allow
- start project by running
make run
to run the project - add your git remote repository by running
git remote add origin github.com/your/xxxx
(DO NOT FORGET TO CREATE YOUR REPOSITORY FIRST IN GitLab) - commit your project to git repository by running
git add . && git commit -m "initial commit" && git push -u origin main
- start coding your project by adding new package in
app/
directory and look at the Guideline How To Code below
./
├── .env
├── .env.template
├── .local/
├── openapi/
├── .scripts/
├── Dockerfile
├── Makefile
├── README.md
├── VERSION
├── app/
│ ├── app.go
│ ├── ticket/
│ │ ├── handler.go
│ │ ├── http_service.go
│ │ ├── interpermit.go
│ │ └── storage_service.go
│ ├── response.go
│ ├── response_writer_middleware.go
│ └── response_writer_middleware_test.go
├── config/
├── database/
├── docker-compose.yml
├── gitlabci.yml
├── go.mod
├── go.sum
├── httpclient/
├── kafka/
├── logger/
├── main.go
├── note.txt
├── redis/
└── serror/
app/
: Business logic and application layer (handler, service, storage)config/
: Configuration files and read from environment variablesdatabase/
: Database connection and schema migration, and database operation general insert, update, delete, and select not specific to any tablehttpclient/
: HTTP client for external APIkafka/
: Kafka producerlogger/
: Logging configurationredis/
: Redis clientserror/
: Custom error package for handling error for the application and business error codemain.go
: Entry point of the applicationMakefile
: Contains all the commands to run the application, test, and build the applicationDockerfile
: Dockerfile for building the applicationdocker-compose.yml
: Docker compose file for running the application and database for local developmentVERSION
: Version file for the application version. This file will be updated by the CI/CD pipelineopenapi/
: API documentation and swagger file.scripts/
: Script for CI/CD and other automation (e.g. CI/CD pipeline, development command).local/
: Local development configuration (e.g. git config pre-commit, pre-push, etc.).env
: Environment variables file for local development (copy from .env.template and fill in the value).env
.template: Environment variables template file for application configuration
- all business logic and application layer should be in this directory. It should be separated by module/package name for example:
-
app/register
-
app/booking
-
app/loan
-
app/credit
-
app/payment
-
app/transfer
-
app/exchange
-
app/purchase
-
each package represent a module/domain in the system follow ideal of DDD (Domain Driven Design)
-
- each package SHOULD have one file name same as package name e.g. package
app/register
should haveregister.go
- this file should contain the struct type and method to interact with the package e.g
type User struct {}
(ortype R struct {}
why? ) ,func New() *User {}
orfunc (r *User) Verify(code string) bool () {...}
- this file should contain the struct type and method to interact with the package e.g
- each package SHOULD have file name to represent responsibility of the file e.g.
handler.go
should contain the handler struct and method to handle the request and responseget_handler.go
orget_user_handler.go
should contain the handler method(h *Handler) Get(w http.ResponseWriter, r *http.Request) {...}
to handle get a user data.gets_handler.go
orget_all_user_handler.go
should contain the handler method(h *Handler) GetAllUsers(w http.ResponseWriter, r *http.Request) {...}
to handle get all user data.create_handler.go
orcreate_user_handler.go
should contain the handler method(h *Handler) Create(w http.ResponseWriter, r *http.Request) {...}
to handle create a user data.dopa_service.go
should contain function to interact with external serviceverify_service.go
should contain function to verify the user data base on the business logicstorage.go
should contain the storage service struct and method to interact with the database
- you CAN DELETE folder
app/interpermit
,app/license
andapp/order
then delete all related endpoint inmain.go
then start create your own feature inapp/
directory.
- all configuration should read then store in the struct in this package
- all database connection and schema migration should be in this package
- all database operation general insert, update, delete, and select not specific to any table should be in this package
- separate file for each database type e.g.:
database/postgres.go
database/mysql.go
database/redis.go
database/kafka.go
- DO NOT put database operation specific to any table/entity in this package e.g.
database/user.go
, please DON'T DO THIS. theuser_storage.go
should be inapp/user/user_storage.go
instead. souser_storage.go
will importdatabase/postgres.go
ordatabase/mysql.go
etc. to interact with the database.
- this is a helper package to interact with external API using HTTP client.
- contain a function to call external API and return the ressult
- it should be use in
app/xxxx
package to interact with external API e.g.app/license/http_service.go
usinghttpclient.Post(url, body, header)
- DO NOT put business logic in this package. the business logic should be in
app/xxxx
package instead.
- this is a helper package to produce message to Kafka
- contain a function to produce message to Kafka
- it should be use in
app/xxxx
package to produce message to Kafka e.g.app/license/storage_kafka.go
usingkafka.Produce(topic, message)
- DO NOT put business logic in this package. the business logic should be in
app/xxxx
package instead.
- this is a helper package to interact with Redis
- contain a function to interact with Redis
- it should be use in
app/xxxx
package to interact with Redis e.g.app/license/storage_redis.go
usingredis.Get(key)
,redis.Set(key, value)
- DO NOT put business logic in this package. the business logic should be in
app/xxxx
package instead.
- this is a helper package to handle error.
- contain a function to handle error and business error code and standardize error response.
- censor the error message before return to the client for security and privacy reason.
- this is a helper package to configure the logger
- config the logger level, format, and output for standardize logging across the application
- contain a mask for sensitive data in the log such as password, token, PII data
- contain a function to log the request and response with PDPA compliance in mind
- it meant to be use in
app/xxxx
package to log the request and response
- this is a helper package to generate API documentation and swagger file
- contain a function to generate API documentation and swagger file
- what is difference between OpenAPI and Swagger?
- The easiest way to understand the difference is:
- OpenAPI = Specification
- Swagger = Tools for implementing the specification
- The easiest way to understand the difference is:
- this is a helper package to run the script for CI/CD and other automation
- contain a script to run the CI/CD pipeline and for all development command
- if you have any command that using frequently you can put it here
- then add your command to the
Makefile
to run the script
- just a simple entry point to start the application. it should be simple and clean as possible leave it in root directory of the project. (same level as go.mod)
- it should be use to initialize the application and start the server
- it should be use to initialize the database connection, redis connection, kafka connection, logger, and other configuration
- it should be use to inject the dependency to the
app/xxxx
package - it should be use to start the server and gracefully shutdown the server
- how to handle multiple main.go?
- single
main.go
scenario: you start just web service as restful API.- you can put
main.go
in the root directory of the project (same level as go.mod)
- you can put
- multiple
main.go
scenario: you start multiple services e.g. web service, batch service, cron service, etc.- you can put
main.go
in thecmd/
directory e.g.cmd/batch/main.go
,cmd/cron/main.go
,cmd/web/main.go
- so each
main.go
will start different service e.g.cmd/batch/main.go
will start batch service,cmd/cron/main.go
will start cron service,cmd/web/main.go
will start web service
- you can put
- we don't have folder
cmd/
in this template. you can create it if you have multiple services.
- single
- Makefile is a simple way to organize commands and tasks for your project. so that when new developers join the project, they can easily run the command to start the project, run the test, build the project, etc. by just looking at the Makefile.
- it should contain all the commands to run the application, test, and build the application
- it should contain all the commands to run the CI/CD pipeline
- it should contain all the commands to run the development command
- Ground rules if you have any command that you use frequently you can put it in the Makefile
- this is a helper package to run the script for CI/CD and other automation
- contain a script to run the CI/CD pipeline and for all development command
- if you have any command that using frequently you can put it here
- then add your command to the
Makefile
to run the script
- this is a helper folder to store the local development configuration
- contain a local development configuration file e.g. git config
pre-commit
,pre-push
, etc. - any local development configuration that your team agreed to use can be put here
make help
: show all available commands in the Makefilemake setup
: setup environment git hook, go mod download and install tools (golangci-lint, govulncheck and swagger)make all
: default (mod inst lint test)make mod
: tidy up the go.mod and go.sum filemake lint
: run golangci-lint to check the code quality and code smell, some basic security check and prevent to use fmt.Print and fmt.Println in the codemake test
: run all the unit testmake coverage
: run all the test with coveragemake vuln
: run govulncheck to check the vulnerabilitymake clean
: remove files created during build pipeline and cachemake precommit
: validate the branch before commitmake docker
: build the docker imagemake run
: run the docker containermake diff
: check if there are any uncommitted changesmake bump-version
: bump the version of the projectmake commit-msg
: run pre-commit to check the commit messagemake swagger
: generate swagger filemake openapi
: serve the swagger file
- Any folder that NOT a Go's package please name it with prefix
.
e.g..scripts/
,.local/
etc. - Any new business logic should be in
app/
directory and should be separated by module/package name.- e.g. if you have a new business logic for
register
module, you should create a new packageapp/register
and put all the business logic in this package.
- e.g. if you have a new business logic for
- SHOULD NOT call
controller
in the package name. It should behandler
instead. - SHOULD NOT create package name by functionally/layers e.g.
app/controller
,app/service
,app/repository
,app/model
- Because in Go, package name should be by domain or business features not by functionally/layers.
- Whenever you facing a CYCLE IMPORT (circular dependency), it's a sign that you are doing it WRONG way.(sometime it cause by package name by functionally/layers)
- Avoid meaningless package names create package name with
common
,utils
,helper
,enums
etc.- Because it's too generic and not specific to any domain or business features.
- variable name should be in MixedCaps (PascalCase) or mixedCaps (camelCase) e.g.
var MaxUsers = 100
,var maxUsers = 100
- local variable name should be in mixedCaps (camelCase) e.g.
var maxUsers = 100
ormaxUsers := 100
- In Go is to use MixedCaps (PascalCase) or mixedCaps (camelCase) rather than underscores to write multiword names.
- a constant name should be in MixedCaps (public constant) or mixedCaps (private constant)
- GOOD:
const MaxUsers = 100
,const maxUsers = 100
- BAD:
const MAX_USERS = 100
,const max_users = 100
(because it's not idiomatic in Go)
- GOOD:
- function name should be in MixedCaps (PascalCase) or mixedCaps (camelCase) e.g.
GetUser
,getOrder
,CreateOrder
,createOrder
- struct name should be in MixedCaps (PascalCase) or mixedCaps (camelCase) e.g.
type Order struct {}
,type order struct {}
-
file name should be in snake_case e.g.
user_handler.go
oruser_storage.go
- GOOD:
user_handler.go
,user_storage.go
,user_http_service.go
,user_http_service_test.go
,user.go
- BAD:
UserHandler.go
,userHandler.go
,UserHandler_test.go
,User.go
- GOOD:
-
file name should represent the responsibility of the file what it does.
- example logger:
logger/logger.go
-
logger/level.go
-
logger/mask.go
-
logger/request_response.go
-
etc.
when you want to make change about log level which file you should look at?
logger/level.go
right? so it's easy to find the file.
-
- example auth:
-
auth/cors.go
-
auth/basic_auth.go
-
auth/jwt.go
-
auth/middleware.go
-
etc.
when you want to make change about basic authenication which file you should look at?
auth/basic_auth.go
right? so it's easy to find the file. -
example register:
register/storage.go
register/dopa_service.go
register/verify_account.go
- etc.
when you want to make change about Department of Provincial Administration (DOPA) service which file you should look at?
register/dopa_service.go
right? so it's easy to find the file.
-
-
file name must create at least one file that same name as package e.g.
package order
should contain atleastorder.go
-
file name
order/xxx_handler.go
it should start with the action of the handler or justorder/handler.go
if it's hold all the handlers. -
we can split file to contain all functionality that it reposible.
-
you can use prefix with action e.g.
get_handler.go
get_all_handler.go
create_handler.go
update_handler.go
delete_handler.go
- etc.
when you read it with their package name it's easy to understand what it does. e.g.
order/get_handler.go
order/get_all_handler.go
order/create_handler.go
order/update_handler.go
order/delete_handler.go
-
-
the test file also follow the same pattern and end with
_test.go
-
the integration test file that test handler-> service/usecase -> storage that end with
_it_test.go
the responsibility of each layer should be separated as file name don't need to create a package for each layer. we can archive layer separation by file name instead of package name for example:
-
order.go
- starting point of the package and model.
- method
s.OrderStore() OrderStore {}
to translate the app (skill) data structure to database data structure
-
order_handler.go or order_get_handler.go or get_handler.go
- struct handler that have Storage Interface as dependency
- Initialize order Storage instance at the
main.go
then inject it into NewOrderHandler(orderStorage) to create a new OrderHandler instance.
-
order_storage.go
-
A Storage struct implements a Storage interface.
-
The Storage struct holds the database connection directly (not through an interface).
-
The Storage struct is initialized in the main.go file.
-
It provides methods for interacting with the database, order entity specific such as:
- FindOrderByKey(key string) (Order, error)
- FindAllOrders(orderBy string, page, limit uint) ([]Order, uint, error)
- CreateOrder(key string, ord OrderStore) error
- UpdateOrder(key string, ord OrderStore) error
-
Go code is organized into packages. Within a package, code can refer to any identifier (name) defined within, while clients of the package may only reference the package’s exported types, functions, constants, and variables. Such references always include the package name as a prefix: foo.Bar refers to the exported name Bar in the imported package named foo.
Good package names make code better. A package’s name provides context for its contents, making it easier for clients to understand what the package is for and how to use it. The name also helps package maintainers determine what does and does not belong in the package as it evolves. Well-named packages make it easier to find the code you need.
Good package names are short and clear. They are lower case, with NO under_scores, NO mixedCaps. They are often simple nouns, such as:
- package time (provides functionality for measuring and displaying time)
- package list (implements a doubly linked list)
- package http (provides HTTP client and server implementations)
these are NOT GOOD for Go package names The style of names typical of another language might not be idiomatic in a Go program. Here are examples of names that might be good style in other languages but do not fit well in Go
- package dopaServiceClient (mixedCaps)
- package http-handler (hyphen)
- package priority_queue (under_score)
Abbreviate judiciously Package names may be abbreviated when the abbreviation is familiar to the programmer. Widely-used packages often have compressed names:
- package strconv (string conversion)
- package syscall (system call)
- package fmt (formatted I/O)
On the other hand, if abbreviating a package name makes it ambiguous or unclear, don’t do it.
Don’t steal good names from the user. Avoid giving a package a name that is commonly used in client code. For example, the buffered I/O package is called bufio, not buf
, since buf
is a good variable name for a buffer.
Avoid repetition. Since client code uses the package name as a prefix when referring to the package contents, the names for those contents need not repeat the package name. for example The HTTP server type in package http.
- GOOD name: Server is a good name
type Server struct {...}
when combined with the package name, it becomes http.Server, which is clear and unambiguous when used in client code. - BAD name:, HTTPServer is a bad name
type HTTPServer struct {...}
when combined with the package name, it becomes http.HTTPServer, which is redundanthttp
.HTTP
verbose when used in client code.
Simplify function names. When a function in package pkg returns a value of type pkg.Pkg (or *pkg.Pkg), the function name can often omit the type name without confusion:
start := time.Now() // start is a time.Time
t, err := time.Parse(time.Kitchen, "6:06PM") // t is a time.Time
ctx = context.WithTimeout(ctx, 10*time.Millisecond) // ctx is a context.Context
ip, ok := userip.FromContext(ctx) // ip is a net.IP
A function named New in package pkg returns a value of type pkg.Pkg. This is a standard entry point for client code using that type:
q := list.New() // q is a *list.List
When a function returns a value of type pkg.T, where T is not Pkg, the function name may include T to make client code easier to understand. A common situation is a package with multiple New-like functions:
d, err := time.ParseDuration("10s") // d is a time.Duration
elapsed := time.Since(start) // elapsed is a time.Duration
ticker := time.NewTicker(d) // ticker is a *time.Ticker
timer := time.NewTimer(d) // timer is a *time.Timer
-
could be named with the -er suffix e.g.
Reader
,Writer
,Formatter
,Parser
,Comparer
,Marshaler
,Unmarshaler
,Closer
,Seeker
,Flusher
,Scanner
etc. when it have a single method and sound make sense. -
keep the interface minimal as possible. Single method is the best. Multiple method is OK. the perfect example of interface in Go is
io.Reader
andio.Writer
interface. it's simple and easy to understand. when you compose it also simple such asio.ReadWriter
orio.ReadCloser
etc. -
could be named with the -able suffix e.g.
Comparable
,Printable
,Readable
,Writable
,Seekable
,Flushable
,Scannable
etc. when it sound make sense. -
could NOT be name with prefix
I
e.g.IReader
,IWriter
,IFormatter
,IParser
,IComparer
,IMarshaler
,IUnmarshaler
,ICloser
,ISeeker
,IFlusher
,IScanner
etc. because it's not idiomatic in Go. -
if you have many methods in the interface you should consider to split it to smaller interface or try to name it with more meaningful name. e.g.
io.ReadWriter
instead ofio.ReaderWriter
orio.ReadCloser
instead ofio.ReaderCloser
etc. if you can't find the meaningful name you should consider to question your design and rethink about it. maybe follow SOLID principle. however, defining the interface and naming it is hard. I leave you with Rob Pike quote "Don't design with interfaces, discover them". -
for example Storage interface it okay to name it with
Storage
e.g.orderStorage
,userStorage
,LicenseStorage
,InterpermitStorage
etc.
type userStorage interface {
SaveUser(context.Context, u User) error
}
struct that implement the interface should be named with stroage
and be private.
type storage struct {
db *sql.DB
}
func (s *storage) SaveUser(ctx context.Context, u User) error {
// implementation
}
then you export way to create the instance of the storage. by using NewStorage
function.
func NewStorage(db *sql.DB) storage {
return storage{db: db}
}
basically you don't need to add word interface
or Interface
into the interface name. it's already clear that it's an interface at compile time.
- log.Fatal or log.Panic should be used ONLY in the main function before the application starts for all hard dependencies/configuration that MUST be available for the application to run.
this repository already have a code to gracefully shutting down the server. you can look at main.go
file to see how it's done.
- Env : Configuration for environment variables because it's simple and no required external dependencies use only Go standard library.
- Viper : Configuration for environment variables because it's complex and required external dependencies use Go standard library instead.
- GORM : ORM for database operation because it's complex and required external dependencies use Go standard library instead. however, you can still use it in simple operation and take your own risk.
- bad naming erros as
Exception
e.g.BaseException.go
,NotFoundException.go
there is NO Exception in Go - bad naming package with
controller
orservice
,repository
- bad naming folder start with src with
src/
,src/common
,src/utils
- bad naming too generic name
common
,utils
,helper
,enums
- Fork this repository
- Clone your forked repository
- Create a new branch for your feature or bug fix
git checkout -b feature/xxxx
orgit checkout -b bugfix/xxxx
- Update update your implementation or changes any design.
- Test your changes
make test
make sure your changes pass all the tests and have test on it if it's possible. - Commit your changes
git add . && git commit -m "your message"
- Push your changes to your forked repository
git push origin feature/xxxx
- Create a new pull request to this repository with explanation of your changes
- Wait for the review and approval from the reviewer
- Merge your pull request
-
SLA สำหรับตั้งค่า timeout ต่างๆ เช่น Read/Write database
- มีค่า default ในกรณีที่ไม่ได้ตั้งค่า config
-
Log
- level อะไรบ้าง และแต่ละ level ต้องการข้อมูลอะไร
-
Gracefully shutting down
-
tracing ID คือค่าอะไร มี span หรือไม่
- install
direnv
to manage environment variables direnv
- install
-
when getting started just
make setup
to install necessary tools- Install gonew
go install golang.org/x/tools/cmd/gonew@latest
- Install golangci-lint
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
- Install govulncheck
go install github.com/ossf/scorecard/v2/cmd/govulncheck@latest
- Install swager
go install github.com/go-swagger/go-swagger/cmd/swagger@latest
- Install gonew
- Standardized Response structure (JSON)
- Auto logging on request and response with PDPA compliance in mind
- Standardized HTTP status code selection(int/out)
- Testable code based
- Code qualities checking on local/CI (golangci-lint, go vet, go test)
- Microservice simple tracing with correlation ID (header X-Ref-Id)
- 12factor, configuration only in environment variables