Introduction
Hello, everyone! 😉 Today, I would like to discuss about configuration for web application on Golang. And not just talk, but show a simple example of a YAML-based configuration for Go web app.
It will be quite a short article, because I don't want to obstruct your information field on purpose! ☝️
📝 Table of contents
Project structure
As you know, I always use Go Modules for my projects (even for the smallest). This demo project is no exception.
$ tree .
.
├── Makefile
├── config.yml
├── go.mod
├── go.sum
└── main.go
-
Makefile
— put all frequently used commands in there. -
config.yml
— config on YAML format. -
main.go
— main file with web app code.
What's YAML?
Follow Wiki page:
YAML (a recursive acronym for "YAML Ain't Markup Language") is a human-readable data-serialization language. It is commonly used for configuration files and in applications where data is being stored or transmitted.
And it's truth! YAML is awesome format to write small or complex configs with understandable structure. Many services and tools, like Docker compose and Kubernetes, uses YAML as main format to describe its configurations.
Golang and YAML
There are many Go packages to work with YAML files. I mostly use go-yaml/yaml (version 2), because it's stable and have nice API.
But you can use any other package you're used to. The essence of it will not change! 😎
Closer look at config file 👀
Let's take a look our (dummy) config file for web app:
# config.yml
server:
host: 127.0.0.1
port: 8080
timeout:
server: 30
read: 15
write: 10
idle: 5
server
— it's root layer of config.
host
, port
and timeout
— options, which we will use later.
✅ Copy-paste repository
Especially for you, I created repository with full code example on my GitHub:
koddr / example-go-config-yaml
Example Go web app with YAML config.
Just git clone
and read instructions from README
.
Let's code!
I built web application's code in an intuitive form. If something is still unclear, please ask questions in comments! 💻
EDIT @ 19 Feb 2020: Many thanks to Jordan Gregory (aka j4ng5y) for huge fixes for my earlier code example. It's really awesome work and I'd like to recommend to follow these new example for all newbie (and not so) gophers! 👍
package main
import (
"context"
"flag"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"gopkg.in/yaml.v2"
)
// Config struct for webapp config
type Config struct {
Server struct {
// Host is the local machine IP Address to bind the HTTP Server to
Host string `yaml:"host"`
// Port is the local machine TCP Port to bind the HTTP Server to
Port string `yaml:"port"`
Timeout struct {
// Server is the general server timeout to use
// for graceful shutdowns
Server time.Duration `yaml:"server"`
// Write is the amount of time to wait until an HTTP server
// write opperation is cancelled
Write time.Duration `yaml:"write"`
// Read is the amount of time to wait until an HTTP server
// read operation is cancelled
Read time.Duration `yaml:"read"`
// Read is the amount of time to wait
// until an IDLE HTTP session is closed
Idle time.Duration `yaml:"idle"`
} `yaml:"timeout"`
} `yaml:"server"`
}
// NewConfig returns a new decoded Config struct
func NewConfig(configPath string) (*Config, error) {
// Create config structure
config := &Config{}
// Open config file
file, err := os.Open(configPath)
if err != nil {
return nil, err
}
defer file.Close()
// Init new YAML decode
d := yaml.NewDecoder(file)
// Start YAML decoding from file
if err := d.Decode(&config); err != nil {
return nil, err
}
return config, nil
}
// ValidateConfigPath just makes sure, that the path provided is a file,
// that can be read
func ValidateConfigPath(path string) error {
s, err := os.Stat(path)
if err != nil {
return err
}
if s.IsDir() {
return fmt.Errorf("'%s' is a directory, not a normal file", path)
}
return nil
}
// ParseFlags will create and parse the CLI flags
// and return the path to be used elsewhere
func ParseFlags() (string, error) {
// String that contains the configured configuration path
var configPath string
// Set up a CLI flag called "-config" to allow users
// to supply the configuration file
flag.StringVar(&configPath, "config", "./config.yml", "path to config file")
// Actually parse the flags
flag.Parse()
// Validate the path first
if err := ValidateConfigPath(configPath); err != nil {
return "", err
}
// Return the configuration path
return configPath, nil
}
// NewRouter generates the router used in the HTTP Server
func NewRouter() *http.ServeMux {
// Create router and define routes and return that router
router := http.NewServeMux()
router.HandleFunc("/welcome", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, you've requested: %s\n", r.URL.Path)
})
return router
}
// Run will run the HTTP Server
func (config Config) Run() {
// Set up a channel to listen to for interrupt signals
var runChan = make(chan os.Signal, 1)
// Set up a context to allow for graceful server shutdowns in the event
// of an OS interrupt (defers the cancel just in case)
ctx, cancel := context.WithTimeout(
context.Background(),
config.Server.Timeout.Server,
)
defer cancel()
// Define server options
server := &http.Server{
Addr: config.Server.Host + ":" + config.Server.Port,
Handler: NewRouter(),
ReadTimeout: config.Server.Timeout.Read * time.Second,
WriteTimeout: config.Server.Timeout.Write * time.Second,
IdleTimeout: config.Server.Timeout.Idle * time.Second,
}
// Handle ctrl+c/ctrl+x interrupt
signal.Notify(runChan, os.Interrupt, syscall.SIGTSTP)
// Alert the user that the server is starting
log.Printf("Server is starting on %s\n", server.Addr)
// Run the server on a new goroutine
go func() {
if err := server.ListenAndServe(); err != nil {
if err == http.ErrServerClosed {
// Normal interrupt operation, ignore
} else {
log.Fatalf("Server failed to start due to err: %v", err)
}
}
}()
// Block on this channel listeninf for those previously defined syscalls assign
// to variable so we can let the user know why the server is shutting down
interrupt := <-runChan
// If we get one of the pre-prescribed syscalls, gracefully terminate the server
// while alerting the user
log.Printf("Server is shutting down due to %+v\n", interrupt)
if err := server.Shutdown(ctx); err != nil {
log.Fatalf("Server was unable to gracefully shutdown due to err: %+v", err)
}
}
// Func main should be as small as possible and do as little as possible by convention
func main() {
// Generate our config based on the config supplied
// by the user in the flags
cfgPath, err := ParseFlags()
if err != nil {
log.Fatal(err)
}
cfg, err := NewConfig(cfgPath)
if err != nil {
log.Fatal(err)
}
// Run the server
cfg.Run()
}
OK! Run it:
$ go run ./...
# OR with different config file
$ go run ./... -config ./static/my-other-config.yml
And finally, go to http://127.0.0.1:8080/welcome
and see message:
Hello, you've requested: /welcome
All done! 🎉
Photo by
[Title] Fabian Grohs https://unsplash.com/photos/dC6Pb2JdAqs
[1] Alfred Rowe https://unsplash.com/photos/FVWTUOIUZd8
P.S.
If you want more articles (like this) on this blog, then post a comment below and subscribe to me. Thanks! 😻
❗️ You can support me on Boosty, both on a permanent and on a one-time basis. All proceeds from this way will go to support my OSS projects and will energize me to create new products and articles for the community.
And of course, you can help me make developers' lives even better! Just connect to one of my projects as a contributor. It's easy!
My main projects that need your help (and stars) 👇
- 🔥 gowebly: A next-generation CLI tool that makes it easy to create amazing web applications with Go on the backend, using htmx, hyperscript or Alpine.js and the most popular CSS frameworks on the frontend.
- ✨ create-go-app: Create a new production-ready project with Go backend, frontend and deploy automation by running one CLI command.
Top comments (7)
I truly appreciate the contribution to the community. That said though, as this feels targeted at newcomers, I personally wouldn't teach "global variables" or "using the init()" function if I could avoid it. Later in life, those two constructs make it harder to test and much harder to find a certain class of bugs, especially when the code base gets a lot bigger. Feel free to ignore me though lol, just my $0.02.
Didn't really understand, when
init()
became an anti-pattern for the Go community? Give me a link to an article about it, please. Same thing about "global variables".Maybe you should write the right article about how to make a Go web app config in the right format? I'd read it, really.
I don't have the slightest idea what you're talking about here. Explain, please. I haven't even met you to ignore you. 🤷♂️
Rather than duplicating the work, I'll just give you an MR on your repo with reference :)
As far as ignoring me, I'm opinionated, so it comes with the territory lol.
Oh, that's would be nice! Thx 😉
But, actually, when
init()
become an "anti-pattern"? Because I seeinit()
on many online books, courses and articles by Go bloggers.I googled it, but I couldn't find any confirmation of your words.
Even the other way around! For example, "Effective Go" book on official Golang website: golang.org/doc/effective_go.html#init
Good article, thank you. I'm working on a personal project and using this opportunity to learn go. I followed your guide mostly, except I have two different configuration files. I wanted to avoid rewriting code, and I was able to abstract away the type of config struct from the YML parsing function using an
interface{}
as an additional parameter. It worked well for me, you can see it here. Thanks again.Thanks for reply! 😉 Yep, separated configs are very helpful.
Interesting project, btw! Keep going 👍
Thanks for this article! It helped me to learn some Go.
Is there any way to set required fields in the config?