Equivalent of Python string.format in Go?

Question:

In Python, you can do this:

"File {file} had error {error}".format(file=myfile, error=err)

or this:

"File %(file)s had error %(error)s" % {"file": myfile, "error": err}

In Go, the simplest option is:

fmt.Sprintf("File %s had error %s", myfile, err)

which doesn’t let you swap the order of the parameters in the format string, which you need to do for I18N. Go does have the template package, which would require something like:

package main

import (
    "bytes"
    "text/template"
    "os"
)

func main() {
    type Params struct {
        File string
        Error string
    }

    var msg bytes.Buffer

    params := &Params{
        File: "abc",
        Error: "def",
    }

    tmpl, _ := template.New("errmsg").Parse("File {{.File}} has error {{.Error}}")
    tmpl.Execute(&msg, params)
    msg.WriteTo(os.Stdout)
}

which seems like a long way to go for an error message. Is there a more reasonable option that allows me to give string parameters independent of order?

Asked By: Scott Deerwester

||

Answers:

I don’t know of any easy way of naming the parameters, but you can easily change the order of the arguments, using explicit argument indexes:

From docs:

In Printf, Sprintf, and Fprintf, the default behavior is for each formatting verb to format successive arguments passed in the call. However, the notation [n] immediately before the verb indicates that the nth one-indexed argument is to be formatted instead. The same notation before a ‘*’ for a width or precision selects the argument index holding the value. After processing a bracketed expression [n], subsequent verbs will use arguments n+1, n+2, etc. unless otherwise directed.

Then you can, ie:

fmt.Printf("File %[2]s had error %[1]s", err, myfile)
Answered By: hlscalon

The parameter can also be a map, so the following function would work if you don’t mind parsing every error format every time you use it:

package main

import (
    "bytes"
    "text/template"
    "fmt"
)

func msg(fmt string, args map[string]interface{}) (str string) {
    var msg bytes.Buffer

    tmpl, err := template.New("errmsg").Parse(fmt)

    if err != nil {
        return fmt
    }

    tmpl.Execute(&msg, args)
    return msg.String()
}

func main() {
    fmt.Printf(msg("File {{.File}} has error {{.Error}}n", map[string]interface{} {
        "File": "abc",
        "Error": "def",
    }))
}

It’s still a little wordier than I would have liked, but it’s better than some other options, I suppose. You could turn map[string]interface{} into a local type and reduce it further to:

type P map[string]interface{}

fmt.Printf(msg("File {{.File}} has error {{.Error}}n", P{
        "File": "abc",
        "Error": "def",
    }))
Answered By: Scott Deerwester

With strings.Replacer

Using strings.Replacer, implementing a formatter of your desire is very easy and compact.

func main() {
    file, err := "/data/test.txt", "file not found"

    log("File {file} had error {error}", "{file}", file, "{error}", err)
}

func log(format string, args ...string) {
    r := strings.NewReplacer(args...)
    fmt.Println(r.Replace(format))
}

Output (try it on the Go Playground):

File /data/test.txt had error file not found

We can make it more pleasant to use by adding the brackets to the parameter names automatically in the log() function:

func main() {
    file, err := "/data/test.txt", "file not found"

    log2("File {file} had error {error}", "file", file, "error", err)
}

func log2(format string, args ...string) {
    for i, v := range args {
        if i%2 == 0 {
            args[i] = "{" + v + "}"
        }
    }
    r := strings.NewReplacer(args...)
    fmt.Println(r.Replace(format))
}

Output (try it on the Go Playground):

File /data/test.txt had error file not found

Yes, you could say that this only accepts string parameter values. This is true. With a little more improvement, this won’t be true:

func main() {
    file, err := "/data/test.txt", 666

    log3("File {file} had error {error}", "file", file, "error", err)
}

func log3(format string, args ...interface{}) {
    args2 := make([]string, len(args))
    for i, v := range args {
        if i%2 == 0 {
            args2[i] = fmt.Sprintf("{%v}", v)
        } else {
            args2[i] = fmt.Sprint(v)
        }
    }
    r := strings.NewReplacer(args2...)
    fmt.Println(r.Replace(format))
}

Output (try it on the Go Playground):

File /data/test.txt had error 666

A variant of this to accept params as a map[string]interface{} and return the result as a string:

type P map[string]interface{}

func main() {
    file, err := "/data/test.txt", 666

    s := log33("File {file} had error {error}", P{"file": file, "error": err})
    fmt.Println(s)
}

func log33(format string, p P) string {
    args, i := make([]string, len(p)*2), 0
    for k, v := range p {
        args[i] = "{" + k + "}"
        args[i+1] = fmt.Sprint(v)
        i += 2
    }
    return strings.NewReplacer(args...).Replace(format)
}

Try it on the Go Playground.

With text/template

Your template solution or proposal is also way too verbose. It can be written as compact as this (error checks omitted):

type P map[string]interface{}

func main() {
    file, err := "/data/test.txt", 666

    log4("File {{.file}} has error {{.error}}", P{"file": file, "error": err})
}

func log4(format string, p P) {
    t := template.Must(template.New("").Parse(format))
    t.Execute(os.Stdout, p)
}

Output (try it on the Go Playground):

File /data/test.txt has error 666

If you want to return the string (instead of printing it to the standard output), you may do it like this (try it on the Go Playground):

func log5(format string, p P) string {
    b := &bytes.Buffer{}
    template.Must(template.New("").Parse(format)).Execute(b, p)
    return b.String()
}

Using explicit argument indices

This was already mentioned in another answer, but to complete it, know that the same explicit argument index may be used arbitrary number of times and thus resulting in the same parameter substituted in multiple times. Read more about this in this question: Replace all variables in Sprintf with same variable

Answered By: icza

Alas, there’s no built-in function in Go for string interpolation with named parameters (yet). But you are not the only one suffering out there 🙂 Some packages should exist, for example: https://github.com/imkira/go-interpol . Or, if feeling adventurous, you could write such a helper yourself, as the concept is actually quite simple.

Cheers,
Dennis

Answered By: oharlem

You can get quite close to that sweet python formatting experience:

message := FormatString("File {file} had error {error}", Items{"file"=myfile, "error"=err})

Declare the following somewhere in your code:

type Items map[string]interface{}

func FormatString(template string, items Items) string {
    for key, value := range items {
        template = strings.ReplaceAll(template, fmt.Sprintf("{%v}", key), fmt.Sprintf("%v", value))
    }
    return template
}
  • note that my implementation is very naive and inefficient for high-performance needs

sudo make me a package

Seeing the development experience potential with having a simple signature like this, I’ve got tempted and uploaded a go package called format.

package main

import (
  "fmt"
  "github.com/jossef/format"
)

func main() {
  formattedString := format.String(`hello "{name}". is lizard? {isLizard}`, format.Items{"name": "Mr Dude", "isLizard": false})
  fmt.Println(formattedString)
}

https://repl.it/@jossef/format

You can try the Go Formatter library that implements replacement fields surrounded by curly braces {} format strings similar to Python format.

Working code example Go Playground:

package main

import (
    "fmt"

    "gitlab.com/tymonx/go-formatter/formatter"
)

func main() {
    formatted, err := formatter.Format("Named placeholders {file}:{line}:{function}():", formatter.Named{
        "line":     3,
        "function": "func1",
        "file":     "dir/file",
    })

    if err != nil {
        panic(err)
    }

    fmt.Println(formatted)
}

Output:

Named placeholders dir/file:3:func1():
Answered By: tymonx

text/template is interesting. I Provide some example below

Usage

func TestFString(t *testing.T) {
    // Example 1
    fs := &FString{}
    fs.MustCompile(`Name: {{.Name}} Msg: {{.Msg}}`, nil)
    fs.MustRender(map[string]interface{}{
        "Name": "Carson",
        "Msg":  123,
    })
    assert.Equal(t, "Name: Carson Msg: 123", fs.Data)
    fs.Clear()

    // Example 2 (with FuncMap)
    funcMap := template.FuncMap{
        "largest": func(slice []float32) float32 {
            if len(slice) == 0 {
                panic(errors.New("empty slice"))
            }
            max := slice[0]
            for _, val := range slice[1:] {
                if val > max {
                    max = val
                }
            }
            return max
        },
        "sayHello": func() string {
            return "Hello"
        },
    }
    fs.MustCompile("{{- if gt .Age 80 -}} Old {{else}} Young {{- end -}}"+ // "-" is for remove empty space
        "{{ sayHello }} {{largest .Numbers}}", // Use the function which you created.
        funcMap)
    fs.MustRender(Context{
        "Age":     90,
        "Numbers": []float32{3, 9, 13.9, 2.1, 7},
    })
    assert.Equal(t, "Old Hello 13.9", fs.Data)
}

Lib

package utils

import (
    "text/template"
)

type Context map[string]interface{}

type FString struct {
    Data     string
    template *template.Template
}

func (fs *FString) MustCompile(expr string, funcMap template.FuncMap) {
    fs.template = template.Must(template.New("f-string").
        Funcs(funcMap).
        Parse(expr))
}

func (fs *FString) Write(b []byte) (n int, err error) {
    fs.Data += string(b)
    return len(b), nil
}

func (fs *FString) Render(context map[string]interface{}) error {
    if err := fs.template.Execute(fs, context); err != nil {
        return err
    }
    return nil
}

func (fs *FString) MustRender(context Context) {
    if err := fs.Render(context); err != nil {
        panic(err)
    }
}

func (fs *FString) Clear() string {
    // return the data and clear it
    out := fs.Data
    fs.Data = ""
    return out
}

Go Playground

important document

Answered By: Carson

Instead of using template.New, where you have to provide a template name, you
can just instantiate a template pointer:

package main

import (
   "strings"
   "text/template"
)

func format(s string, v interface{}) string {
   t, b := new(template.Template), new(strings.Builder)
   template.Must(t.Parse(s)).Execute(b, v)
   return b.String()
}

func main() {
   params := struct{File, Error string}{"abc", "def"}
   println(format("File {{.File}} has error {{.Error}}", params))
}
Answered By: Zombo

Use os.Expand to replace fields in a format string. Expand replaces ${var} or $var in the string using a func(string) string mapping function.

Here are a couple of ways to wrap os.Expand in convenient to use functions:

func expandMap(s string, m map[string]string) string {
    return os.Expand(s, func(k string) string { return m[k] })
}

func expandArgs(s string, kvs ...string) string {
    return os.Expand(s, func(k string) string {
        for i := 1; i < len(kvs); i++ {
            if kvs[i-1] == k {
                return kvs[i]
            }
        }
        return ""
    })
}

Example use:

s = expandMap("File ${file} had error ${error}",
       map[string]string{"file": "myfile.txt", "error": "Not found"})

s = expandArgs("File ${file} had error ${error}", 
      "file", "myfile.txt", "error", "Not found"))

Run the code on the playground.

Answered By: Cerise Limón

Here is a function I wrote which replaces fields with strings in a map, similar to what you can do with Python. It takes a string which should have fields that look like ${field} and replaces them with any such keys in the given map like map['field']='value':

func replaceMap(s string,m *map[string]string) string {
        r := regexp.MustCompile("\${[^}]*}")
        for x,i := range *m {
                s = strings.Replace(s,"${"+x+"}",i,-1)
        }
        // Remove missing parameters
        s = r.ReplaceAllString(s,"")
        return s
}

Playground example:
https://go.dev/play/p/S5rF5KLooWq

Answered By: Brad
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.