DEV Community

Cover image for Let's write config for your Golang web app on right way — YAML 👌
Vic Shóstak
Vic Shóstak

Posted on • Edited on

Let's write config for your Golang web app on right way — YAML 👌

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


Enter fullscreen mode Exit fullscreen mode
  • 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


Enter fullscreen mode Exit fullscreen mode

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()
}


Enter fullscreen mode Exit fullscreen mode

OK! Run it:



$ go run ./...

# OR with different config file

$ go run ./... -config ./static/my-other-config.yml


Enter fullscreen mode Exit fullscreen mode

And finally, go to http://127.0.0.1:8080/welcome and see message:



Hello, you've requested: /welcome


Enter fullscreen mode Exit fullscreen mode

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.

support me on Boosty

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.

Other my small projects: yatr, gosl, json2csv, csv2api.

Top comments (7)

Collapse
 
j4ng5y profile image
Jordan Gregory • Edited

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.

Collapse
 
koddr profile image
Vic Shóstak • Edited

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. 🤷‍♂️

Collapse
 
j4ng5y profile image
Jordan Gregory

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.

Thread Thread
 
koddr profile image
Vic Shóstak • Edited

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

Collapse
 
dadatuputi profile image
Bradford

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.

Collapse
 
koddr profile image
Vic Shóstak

Thanks for reply! 😉 Yep, separated configs are very helpful.

Interesting project, btw! Keep going 👍

Collapse
 
1oglop1 profile image
Jan Gazda

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