Stringer: Go 语言的字符串生成工具

Golang stringer 命令行工具详解

文章目录

本文将介绍 Golang 中的 stringer 工具,它可以帮助你自动生成 String() 方法,从而使代码更易于维护和理解。我们将从 Stringer 接口的基本用法入手,展示如何自定义类型的字符串表示,然后介绍 stringer 命令行工具,包括安装、使用方法和一些高级技巧。

使用 Stringer 接口自定义类型的字符串表示

在 Golang 开发中,我们经常需要将代码中的标识符,例如枚举常量,转换成易于理解的字符串形式。例如,你可能有一个表示状态码的整数类型,但你希望在日志或者用户界面上显示更友好的文本,例如 “Status OK” 而不是 “200”。

这时,fmt 包提供了一个名为 Stringer 的通用接口,它可以帮助我们自定义类型的字符串表示。许多包都会用到它,该接口只有一个 String() 方法。任何需要字符串形式标识符的包通常会寻找此接口的实现。

type Stringer interface {
    String() string
}

因此,在打印任何标识符时,你可以通过为其定义一个 String() 方法来控制实际打印的内容。

自定义字符串表示的优势

假设你有一个名为 StatusCode 的整数类型和一些该类型的常量。如果使用 Println(来自 fmtlog)打印这些常量,你将看到打印的是状态码(200, 500 等)。

type StatusCode int

const (
    statusOK              StatusCode = 200
    statusInternalServerErr StatusCode = 500
)

func main() {
    fmt.Println(statusOK)
    fmt.Println(statusInternalServerErr)
    // Output:
    // 200
    // 500
}

但如果你想看到这些状态码的字符串表示,你可以通过简单地为 StatusCode 定义一个 String() 方法来实现 Stringer 接口。现在,Println 将打印 String() 函数返回的任何内容。

type StatusCode int

func (c StatusCode) String() string {
    switch c {
    case 200:
        return "Status OK"
    case 500:
        return "Internal server error"
    default:
        return "Unknown status"
    }
}

const (
    statusOK              StatusCode = 200
    statusInternalServerErr StatusCode = 500
)

func main() {
    fmt.Println(statusOK)
    fmt.Println(statusInternalServerErr)
    // Output:
    // Status OK
    // Internal server error
}

Stringer 在以下用例中非常有用:

  1. 遥测/日志记录:代码中的标识符(尤其是数字和结构体)很难在日志语句和指标标签中表示。数字可以直接使用,但对于那些对代码上下文了解较少或没有上下文的人来说,可能难以理解。String() 方法可以帮助你为日志/指标标签中的这些标识符定义一个合理的表示形式。
  2. 用户侧:类似地,代码可能处理某些与用户看到或输入的值不同的值。Stringer 接口在这方面也能提供帮助。

可能存在的问题

手动维护 String() 方法中的 switch case 语句非常繁琐。你创建的每个常量都需要一个 case。如果你忘记添加一个,可能会导致不一致的行为。

使用 stringer 命令行工具自动生成 String() 方法

为了解决手动维护 String() 方法可能存在的问题,Go 的标准工具链提供了 stringer 命令行工具,它可以自动生成这些 String() 方法。默认情况下,常量名称将用作字符串值,但也提供了一些标志来控制它。

安装 stringer 命令行工具

go install golang.org/x/tools/cmd/stringer@latest

stringer 工具用法示例

假设有以下代码:

package main

import "fmt"

type Status int

const (
	statusOK                Status = 200
	statusBadRequest        Status = 400
	statusInternalServerErr Status = 500 // Something went wrong
)

func main() {
	fmt.Println(statusOK)
	fmt.Println(statusBadRequest)
	fmt.Println(statusInternalServerErr)
}

将其粘贴到 main.go 文件中并直接运行,将得到以下输出:

200
400
500

现在,在与此文件相同的目录中,运行以下命令:

$ stringer --type Status

这将生成一个名为 status_string.go 的文件,其中包含自动生成的 String() 方法,生成的文件内容如下:

// Code generated by "stringer --type Status"; DO NOT EDIT.

package main

import "strconv"

func _() {
	// An "invalid array index" compiler error signifies that the constant values have changed.
	// Re-run the stringer command to generate them again.
	var x [1]struct{}
	_ = x[statusOK-200]
	_ = x[statusBadRequest-400]
	_ = x[statusInternalServerErr-500]
}

const (
	_Status_name_0 = "statusOK"
	_Status_name_1 = "statusBadRequest"
	_Status_name_2 = "statusInternalServerErr"
)

func (i Status) String() string {
	switch {
	case i == 200:
		return _Status_name_0
	case i == 400:
		return _Status_name_1
	case i == 500:
		return _Status_name_2
	default:
		return "Status(" + strconv.FormatInt(int64(i), 10) + ")"
	}
}

再次运行 Go 程序现在应该给出如下输出:

statusOK
statusBadRequest
statusInternalServerErr

打印这些特定值是因为自动生成的 String() 返回的值默认基于标识符名称。--type 标志是必需的,它接受需要实现 Stringer 接口的类型名称。

--type 标志可以接受多个以逗号分隔的类型名称

例如:--type Status,Method,Error

标识符以 status 为前缀,以指示它是 Status 类型。但在结果字符串值中,不需要这样做(因为类型在输出中并不真正相关)。可以使用 --trimprefix 标志将其删除。

$ stringer --type Status --trimprefix status

运行上述命令并再次运行 Go 程序。现在,输出应如下所示:

OK
BadRequest
InternalServerErr

如果想要自定义值,可以使用 --linecomment 标志,该标志允许你指定 String() 要返回的确切值。对代码进行以下更改:

const (
    statusOK              Status = 200
    statusBadRequest      Status = 400
    statusInternalServerErr Status = 500 // Something went wrong
)
$ stringer --type Status --trimprefix status --linecomment

运行上述命令后,Go 程序应给出以下输出:

OK
BadRequest
Something went wrong

go:generate:简化 stringer 的使用

你可以通过添加 //go:generate <shell command> 到你的 Go 文件中来自动化很多事情。

借助 go generate,你可以自动化运行 stringer 命令。这样做将解决之前提到的所有问题:

  • 不必为所有类型运行 stringer

  • 你可以通过运行以下命令为所有子包运行一次 stringer

    $ go generate ./...
    

在将整数类型声明为枚举时,只需在其上方添加此 go:generate 注释。在哪里添加此注释并不重要。但是将此注释放在类型声明的正上方将有助于读者理解将为此类型自动生成 String() 方法。

//go:generate stringer -type Status -trimprefix status
type Status int

const (
    statusOK              Status = 200
    statusBadRequest      Status = 400
    statusInternalServerErr Status = 500
)

不必将生成的 _string.go 文件检入 git。你只需在 CI 中添加一个额外的步骤,就在构建步骤之前,运行 go generate ./...。所有 String() 方法将自动生成。

注意事项

  • 如果你有 go:generate 注释,请务必在运行 go build 之前始终运行 go generate。因为,如果不运行 go generate,构建命令可能不会失败(如果未直接使用 String() 函数)。但是最终可能导致不一致的行为。
  • go generate 不保证执行顺序。因此,如果你关心生成字符串方法的顺序,请确保编写自己的业务流程脚本。

总结

stringer 是一个能提升代码可维护性和可读性的实用工具。通过自动生成 String() 方法,它可以帮助开发者避免手动编写繁琐的 switch case 语句,减少出错的可能性。通过合理使用 stringer 工具,你可以编写出更清晰、更易于理解和维护的 Golang 代码。它和 gofmt, goimports, gopls, godoc 等工具一样,都是 Go 语言提供的强大工具,可以帮助你编写更高质量的代码。

希望本文能够帮助你更好地理解和使用 stringer 工具,并在你的 Golang 项目中发挥它的优势。


也可以看看