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

Vic ShΓ³stak - Feb 13 '20 - - Dev Community

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.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player