Discussion on Go configuration management

Keywords: Go config

catalogue

  • Configuration classification
  • Best practices
  • configuration management
  • Reference link

1. Configuration classification

Environment configuration

The information that the application should determine during deployment should not be written in the configuration file or configuration center, but should be injected by the deployment platform when the application is started, for example k8s directly when the container is started

Static configuration

The information required during application resource initialization, such as basic information of services, Mysql, Redis, Mongodb and other configuration information, is generally a static file placed in the root directory of the project

Dynamic configuration

Some configurations are required when the application is running, similar to some function switches, which are generally controlled in the management background

2. Best practices

2.1 mode I

json static file management configuration file configuration

{
  "server": {
    "addr": "0.0.0.0:8000"
  },
  "mysql": {
    "driver": "mysql",
    "dsn": "root:@tcp(127.0.0.1:3306)/testdb?parseTime=True"
  }
}

The project depends on the global Config map

package main

import (
   "database/sql"
   "encoding/json"
   "net/http"
   "os"
)

var Config = make(map[string]map[string]string)

func main() {

   file, err := os.Open("/path/to/config/config.json")
   if err != nil {
      panic(err)
   }
   decoder := json.NewDecoder(file)
   err = decoder.Decode(&Config)
   if err != nil {
      panic(err)
   }
     defer file.Close()
  
   db, err := NewMysql(Config["mysql"]["driver"], Config["mysql"]["driver"])
   if err != nil {
      panic(err)
   }
   server := &http.Server{
      Addr: Config["server"]["addr"],
   }
   err = server.ListenAndServe()
   if err != nil {
      panic(err)
   }
}

// Connect to mysql
func NewMysql(driver, dsn string) (*sql.DB, error) {
   return sql.Open(driver, dsn)
}

2.2 mode II

Create a Config structure type, the business depends on this structure, and resolve the json configuration to this structure

import (
   "database/sql"
   "encoding/json"
   "net/http"
   "os"
)

var Config C

func main() {

   file, err := os.Open("/path/to/config/config.json")
   if err != nil {
      panic(err)
   }
   decoder := json.NewDecoder(file)
   err = decoder.Decode(&Config)
   if err != nil {
      panic(err)
   }
   defer file.Close()
   
   db, err := NewMysql(Config.Mysql)

   if err != nil {
      panic(err)
   }
   server := &http.Server{
      Addr: Config.Server.Addr,
   }
   err = server.ListenAndServe()
   if err != nil {
      panic(err)
   }
}

type C struct {
   Server *Server
   Mysql  *Mysql
}

type Server struct {
   Addr string `json:"addr"`
}

type Mysql struct {
   Driver string `json:"driver"`
   Dsn    string `json:"dsn"`
}

func NewMysql(mysql *Mysql) (*sql.DB, error) {
   return sql.Open(mysql.Driver, mysql.Dsn)
}

3. Configuration management

Redis Server configuration

// Server defines options for redis cluster.
type Server struct {
    Addr         string
    Password     string
    Database     int
    DialTimeout  time.Duration     
}
  • Required item cannot be blank

    • Address Addr
    • Password password
  • Optional items can be blank and have default values

    • Database database
    • Timeout

For the above configuration, there are multiple ways to construct

3.1 method 1: multiple function signatures

Create a generic constructor

func NewServer(addr string, password string) (*Server, error) {
    return &Server{addr, password, 0, time.Second}, nil
}

Customize the database and timeout. Go does not support function overloading. Use different function signatures to implement the corresponding configuration

func NewServerWithDatabase(addr string, password string,database int)(*Server, error){
    return &Server{addr, password, database, time.Second}, nil
}

func NewServerWithTimeout(addr string, password string,timeout time.Duration)(*Server, error){
    return &Server{addr, password, 0, timeout}, nil
}

Disadvantages:

  • If we want to customize more requirements, don't we add a function signature for each requirement
  • Function parameters will be very long, which is not conducive to expansion
  • Not an elegant gesture

3.2 mode 2: transfer configuration structure

Pass a configured structure and put all the optional items into it to solve the long parameters and various user-defined requirements

type Server struct {
    Addr     string
    Password string
    Option   *Option
}

type Option struct {
    Database    int
    DialTimeout time.Duration
}
func NewServer(addr string, password string, option *Option) (*Server, error) {
    return &Server{addr, password, option}, nil
}


func NewServer(addr string, password string,option *Option) (*Server, error) {
    return &Server{addr, password, option}, nil
}

call

NewServer("127.0.0.1:6379", "password", &Option{Database: 1, DialTimeout: time.Second})

Disadvantages:

  • For internal use, you need to first judge whether the pointer is nil
  • Because a pointer is passed, it can be modified outside the function. This is not the expected behavior, and unpredictable things will happen
  • There is no way to distinguish between default values

3.3 mode 3: Builder mode

// ServerBuilder uses a builder class as a wrapper
type ServerBuilder struct {
   Server
   Err error
}

func (s *ServerBuilder) WithAddr(addr string) *ServerBuilder {
   s.Server.Addr = addr
   return s
}

func (s *ServerBuilder) WithPassword(pwd string) *ServerBuilder {
   s.Server.Password = pwd
   return s
}

func (s *ServerBuilder) WithTimeout(tw time.Duration) *ServerBuilder {
   s.Server.Option.DialTimeout = tw
   return s
}

func (s *ServerBuilder) WithDatabase(db int) *ServerBuilder {
   s.Server.Option.Database = db
   return s
}

func (s *ServerBuilder) Build() Server {
   return s.Server
}

call

builder := ServerBuilder{}
builder.WithAddr("127.0.0.1:6379").WithPassword("pwd").WithTimeout(2 * time.Second).WithDatabase(8).Build()

Disadvantages:

  • You need to add a new structure to package
  • If you do not add a new structure and build it directly on the Server, you need to add an error member when handling errors, which is impure

3.4 mode 4: Function Optional

Define a function type

type Opt func(option *Option)

Define correlation function

func WithDatabase(database int) Opt {
    return func(option *Option) {
        option.Database = database
    }
}

func WithTimeout(to time.Duration) Opt {
    return func(option *Option) {
        option.DialTimeout = to
    }
}

func NewServer(addr string, password string, opts ...Opt) (*Server, error) {

    option := &Option{
        Database:    0,
        DialTimeout: time.Second,
    }
    for _, opt := range opts {
        opt(option)
    }
    return &Server{
        Addr: addr,
        Password: password,
        Option:option,
    },nil
}

call

NewServer("127.0.0.1:6379", "password", WithTimeout(2*time.Second), WithDatabase(8))

Advantages:

  • Order independent
  • Better maintainability and scalability
  • More intuitive and lower cost
  • Nothing confusing (nil or empty)

4. Reference link

Posted by loosus on Mon, 29 Nov 2021 19:11:39 -0800