loading...
Cover image for Let's write config for your Golang web app on right way β€” YAML πŸ‘Œ

Let's write config for your Golang web app on right way β€” YAML πŸ‘Œ

koddr profile image Vic ShΓ³stak Updated on ・5 min read

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! 😎

config file

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:

GitHub logo 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 β€” write a comment below & follow me. Thx! 😘

Posted on by:

koddr profile

Vic ShΓ³stak

@koddr

Hey! πŸ‘‹ I'm founder and full stack web developer (Go, JavaScript, Docker & automation) at True web artisans. Golang lover, UX evangelist, DX philosopher & UI Dreamer with over 12+ years of experience.

Discussion

pic
Editor guide
 

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 πŸ‘

 

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.

Feel free to ignore me though lol, just my $0.02.

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 see init() 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

 

Thanks for this article! It helped me to learn some Go.
Is there any way to set required fields in the config?