Go语言中类似于Python的字符串格式化函数是什么?

56
在Python中,你可以这样做:
"File {file} had error {error}".format(file=myfile, error=err)

或者这个:
"File %(file)s had error %(error)s" % {"file": myfile, "error": err}

在Go中,最简单的选项是:
fmt.Sprintf("File %s had error %s", myfile, err)

该程序不允许您交换格式字符串中的参数顺序,而这是您需要为I18N做的。Go确实有template包,但需要类似以下的操作:
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)
}

似乎这是一个冗长的错误信息。是否有更合理的选项,可以让我独立于顺序提供字符串参数?
10个回答

61

使用 strings.Replacer

使用strings.Replacer非常简单和紧凑,可以实现您所需的格式化程序。

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

输出结果(在Go Playground上尝试):

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

我们可以通过在log()函数中自动添加参数名称的括号来使其更易于使用:

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

输出(在Go Playground上尝试):

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

是的,你可以说这只接受字符串参数值。这是真的。通过一些改进,这将不再是真的:

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

输出结果(在Go Playground上尝试):

File /data/test.txt had error 666

这是一种将参数作为 map[string]interface{} 接受并将结果作为 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)
}

可以在Go Playground上尝试一下。

使用text/template

您的模板解决方案或建议也太冗长了。它可以写得像这样紧凑(省略错误检查):

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

输出结果(在Go Playground上试一试):

File /data/test.txt has error 666

如果你想返回字符串(而不是将其打印到标准输出),可以像这样做(在Go Playground上尝试):
func log5(format string, p P) string {
    b := &bytes.Buffer{}
    template.Must(template.New("").Parse(format)).Execute(b, p)
    return b.String()
}

使用显式参数索引

这已经在另一个答案中提到,但为了完整起见,需要知道相同的显式参数索引可以任意多次使用,从而导致同一参数被多次替换。有关此内容的更多信息,请阅读以下问题:使用相同变量代替Sprintf中的所有变量


我想你也可以将我的答案中的方法与下面的 map 结合起来,然后使用 log3 中的方法,将参数放入一个 map 而不是一个字符串数组中,并像在 log4 中一样进行格式化。 我的示例中很多复杂性都是为了实现返回一个字符串。 你能修改 log4 来适应这个吗? - Scott Deerwester
@ScottDeerwester 是的,我添加了一个带有map参数并将结果作为string返回的log33()变体,还添加了一个返回结果而不是将其打印到标准输出的log5()变体。 - icza
@icza:感谢您提供如此精彩的答案。我想知道,将近一年后,是否有更好的方法来完成这个任务?最终,我选择了strings.Replacer,它似乎可以很好地工作。 - elo80ka
@elo80ka 我不知道有没有改变。你有和一年前同样的选择。 - icza

26

我不知道有什么简单的方法来命名参数,但是您可以使用显式参数索引轻松更改参数的顺序:

来自文档

在Printf,Sprintf和Fprintf中,默认行为是对调用中传递的连续参数格式化每个格式化动词。但是,在动词之前立即加上 [n] 表示要格式化第 n 个从一开始计数的参数。在宽度或精度的 '*' 之前相同的符号选择保留该值的参数索引。在处理了带括号的表达式 [n] 之后,随后的动词将使用参数 n+1,n+2 等,除非另有指示。

然后您可以这样做,例如:

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

4
参数也可以是一个映射,因此如果您不介意每次使用时解析每个错误格式,则以下函数将起作用:
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",
    }))
}

虽然比我想象中的要冗长一些,但我认为这比其他选项要好。你可以将map[string]interface{}转换为本地类型,并将其进一步简化为:

type P map[string]interface{}

fmt.Printf(msg("File {{.File}} has error {{.Error}}\n", P{
        "File": "abc",
        "Error": "def",
    }))

3

使用os.Expand函数替换格式字符串中的字段。Expand函数通过一个func(string) string映射函数,将字符串中的${var}或$var替换为对应的值。

下面是一些方便使用的函数,可以将os.Expand进行包装:

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

使用示例:

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"))

在 playground 上运行代码


2

哎呀,目前 Go 语言还没有内置的函数支持带有命名参数的字符串插值。但你并不是唯一一个受苦的人 :) 一些包已经存在,例如:https://github.com/imkira/go-interpol。或者,如果你感到冒险,你也可以自己编写这样的辅助函数,因为这个概念实际上非常简单。

祝好, Dennis


2
你可以尝试使用Go Formatter库,该库实现了用花括号{}包裹的替换字段格式化字符串,类似于Python的格式化方式。
以下是可运行的代码示例: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)
}

输出:

Named placeholders dir/file:3:func1():

2

不必使用template.New,在那里您需要提供模板名称,您可以直接实例化一个模板指针:

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

0

text/template 很有趣。我在下面提供了一些示例

用法

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

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

重要文件


0
这是我编写的一个函数,它可以将映射中的字段替换为字符串,类似于Python中的操作。它接受一个字符串,其中应该包含看起来像${field}的字段,并用给定映射中的任何这样的键(例如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
}


示例游乐场: https://go.dev/play/p/S5rF5KLooWq

0

你可以接近那种美妙的Python格式化体验

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

在你的代码中的某处声明以下内容:

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
}
  • 请注意,我的实现对于高性能需求非常天真和低效。

sudo make me a package

看到这样一个简单的签名,我意识到了它所具有的开发体验潜力,因此我上传了一个名为format的Go包。

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接