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?