Home > Enterprise >  Go: How to create a server which can serve urls described in config file
Go: How to create a server which can serve urls described in config file

Time:04-29

could anyone help me here please as I'm new to golang? I have a yaml file which looks like this:

port: 5000
handlers:
  - name: test1
    uri: /api/test1
    response:
      status: 200
      body: test1
  - name: test2
    uri: /api/test2
    response:
      status: 500
      body: test2

based on this file I want to create a server. Currently I'm trying to do it this way, but looks like it doesn't work as expected. What am I doing wrong and what is the better way to achieve what I need?

package main

import (
    "fmt"
    "io/ioutil"
    "log"
    "net/http"

    "gopkg.in/yaml.v2"
)

func main() {
    config := parseYaml("conf.yaml")
    configHandlers := config.Handlers
    mux := http.NewServeMux()
    for _, handler := range *configHandlers {
        mux.HandleFunc(*handler.Uri, func(w http.ResponseWriter, r *http.Request) {
            w.WriteHeader(*handler.Response.Status)
            fmt.Fprintf(w, *handler.Response.Body)
        })
    }
    log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", *config.Port), mux))
}

type YamlConfig struct {
    Port     *string          `yaml:"port"`
    Handlers *[]HandlerConfig `yaml:"handlers"`
}

type HandlerConfig struct {
    Uri      *string   `yaml:"uri"`
    Name     *string   `yaml:"name"`
    Response *Response `yaml:"response"`
}

type Response struct {
    Status *int    `yaml:"status"`
    Body   *string `yaml:"body"`
}

func (c *YamlConfig) parseYaml(data []byte) error {
    return yaml.Unmarshal(data, c)
}

func parseYaml(path string) YamlConfig {
    data, err := ioutil.ReadFile(path)
    if err != nil {
        log.Fatal(err)
    }
    var config YamlConfig
    if err := config.parseYaml(data); err != nil {
        log.Fatal(err)
    }
    return config
}

Update: If I run this server then regardless of which endpoint I hit, it will always return me 500 and test2 in body

CodePudding user response:

What you're seeing is seemingly a common pitfall for people:

configHandlers := config.Handlers
mux := http.NewServeMux()
for _, handler := range *configHandlers {
    mux.HandleFunc(*handler.Uri, func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(*handler.Response.Status)
        fmt.Fprintf(w, *handler.Response.Body)
    })
}

The for loop, on each iteration, reassigns the handler variable. In the loop body, you create a new function and pass it to mux.HandlerFun. These function bodies kind of inherit the outer scope, and access this handler variable. The variable is reassigned outside of the functions, and thus the values each handler function has access to changes with it. What you can do to address the issue is mask the handler variable the loop uses, and create a scope that is unique to each handler. The classic way in languages like JavaScript (where this is - or used to be back when I wrote some JS - a common issue) is to wrap the code in an IIFE (Immediately Invoked Function Expression):

for _, handler := range *configHandlers {
    func (handler *HandlerConfig) { // handler is now the argument passed to this function
        mux.HandleFunc(*handler.Uri, func(w http.ResponseWriter, r *http.Request) {
            w.WriteHeader(*handler.Response.Status)
            fmt.Fprintf(w, *handler.Response.Body)
        })
    }(handler) // call the function with the _current_ value of handler
}

This is a tad messy, and because golang is properly block-scoped, you can just do this:

for _, handler := range *configHandlers {
    h := handler // create a variable in the inner scope
    mux.HandleFunc(*handler.Uri, func(w http.ResponseWriter, r *http.Request) {
        // now h will reference a copy unique to each iteration
        w.WriteHeader(*h.Response.Status)
        fmt.Fprintf(w, *h.Response.Body)
    })
}

That ought to fix it. I've noticed some weirdness with your use of pointers in the types you've added to your question, though... Fields like Port being of type *string? Why wouldn't you just use string? No Same for the Body and Status fields in the Response type. By changing them to plain string fields you don't have to dereference them in your handler functions. It will look a lot cleaner.

A bigger worry is this field:

Handlers *[]HandlerConfig `yaml:"handlers"`

I'm not sure if you really know what the type of this field is, but it makes next to no sense. Handlers is now a pointer to a slice of HandlerConfig values. I'm assuming you wanted this field to be:

// Handlers is a slice of HandlerConfig values:
Handlers []HandlerConfig `yaml:"handlers"`
// or Handlers is a slice of pointers to HandlerConfig values
Handlers []*HandlerConfig `yaml:"handlers"`

Generally speaking, a pointer to a slice, especially in a config type is bad code.

CodePudding user response:

If you define a struct that will represent the configuration in your YAML file, you can unmarshall the yaml into an instantiated struct of that type using the yaml package. From there, you can reference the fields in the struct as any other struct.

package main

import (
    "fmt"

    "gopkg.in/yaml.v2"
)

type YamlExample struct {
    FieldOne    string `yaml:"fieldOne"`
    NestedField struct {
        Name string `yaml:"name"`
    } `yaml:"nestedField"`
}

const YamlEx string = `
fieldOne: one
nestedField:
    name: nestedFieldName
`

func main() {
    var yamlE YamlExample

    err := yaml.Unmarshal([]byte(YamlEx), &yamlE)
    if err != nil {
        panic(err)
    }

    fmt.Printf("% v\n", yamlE)
}

Link to example.

In your case, you'd probably want to handle the routes in a struct and then reference the fields in the struct for things like route name, how to handle the body of the request, etc. If your YAML is stored in a file, you'll have to use something like the io package to read the file into a byte array that the YAML package can parse. See here for a reference.

  • Related