1. 程式人生 > 其它 >Go fmt包

Go fmt包

fmt包簡介

我們經常會使用 fmt 包,但是卻沒有思考過它的實現。我們會在這裡使用一個 fmt.Printf,又會在那裡使用一個 fmt.Sprintf。但是,如果你仔細研究下這個包,你會發現很多有趣有用的東西。

由於 Go 在大多數情況下會用來編寫伺服器或服務程式,我們主要的除錯工具就是日誌系統。log 包提供的 log.Printf 函式有和 fmt.Printf 相同的語義。 良好且資訊豐富的日誌訊息非常重要,並且如果為你的資料結構新增一些格式化的支援將會為你的日誌訊息增加有價值的資訊。

格式化輸出

Go 的 fmt 相關的函式支援一些佔位符,最常見的是字串佔位符的 %s,整型佔位符 %d

,以及浮點型佔位符 %f。現在讓我們探究一些其他的佔位符。

%v & %T

%v 佔位符可以列印任何 Go 的值,%T 可以打印出變數的型別。我經常使用這些佔位符來除錯程式。

Copy
var e interface{} = 2.7182
fmt.Printf("e = %v (%T)\n", e, e) // e = 2.7182 (float64)

寬度

你可以為一個列印的數值指定寬段,比如:

Copy
fmt.Printf("%10d\n", 353)  // will print "       353"

你還可以通過將寬度指定為 * 來將寬度當作 Printf 的引數,例如:

Copy
fmt.Printf("%*d\n"
, 10, 353) // will print " 353"

當你打印出數字列表而且希望它們能夠靠右對齊時,這非常的有用。

Copy
// alignSize return the required size for aligning all numbers in nums
func alignSize(nums []int) int {
    size := 0
    for _, n := range nums {
        if s := int(math.Log10(float64(n))) + 1; s > size {
            size = s
        }
    }

    return
size } func main() { nums := []int{12, 237, 3878, 3} size := alignSize(nums) for i, n := range nums { fmt.Printf("%02d %*d\n", i, size, n) } }

將會打印出:

Copy
00   12
01  237
02 3878
03    3

這使得我們更加容易比較數字。

通過位置引用

如果你在一個格式化的字串中多次引用一個變數,你可以使用 %[n],其中 n 是你的引數索引(位置,從 1 開始)。

Copy
fmt.Printf("The price of %[1]s was $%[2]d. $%[2]d! imagine that.\n", "carrot", 23)

這將會打印出:

Copy
The price of carrot was $23. $23! imagine that.

%v

%v 佔位符將會打印出 Go 的值,如果此佔位符以 + 作為字首,將會打印出結構體的欄位名,如果以 # 作為字首,那麼它會打印出結構體的欄位名和型別。

Copy
// Point is a 2D point
type Point struct {
    X int
    Y int
}

func main() {
    p := &Point{1, 2}
    fmt.Printf("%v %+v %#v \n", p, p, p)
}

這將會列印:

Copy
&{1 2} &{X:1 Y:2} &main.Point{X:1, Y:2}

我經常會使用 %+v 這種佔位符。

fmt.Stringer & fmt.Formatter

有時候你希望能夠精細化地控制你的物件如何被列印。例如,當向用戶展示錯誤時,你可能需要的是一個字串的表示,而當向日志系統寫入時,你則希望是更加詳細的字串表示。

為了控制你的物件如何被列印,你可以實現 fmt.Formatter 介面,也可以選擇實現 fmt.Stringer 介面。

使用 fmt.Formatter 介面比較好的例子是 github.com/pkg/errors 這個非常棒的庫。假設你需要載入我們的配置檔案,但是你有一個錯誤。你可以向用戶列印一個簡短的錯誤(又或者在 API 中返回),並在日誌中輸出更加詳細的錯誤。

Copy
cfg, err := loadConfig("/no/such/config.toml")
if err != nil {
    fmt.Printf("error: %s\n", err)
    log.Printf("can't load config\n%+v", err)
}

這就會向用戶展示:

Copy
error: can't open config file: open /no/such/file.toml: no such file or directory

並且,日誌檔案中會這麼記錄:

Copy
2018/11/28 10:43:00 can't load config
open /no/such/file.toml: no such file or directory
can't open config file
main.loadConfig
    /home/miki/Projects/gopheracademy-web/content/advent-2018/fmt.go:101
main.main
    /home/miki/Projects/gopheracademy-web/content/advent-2018/fmt.go:135
runtime.main
    /usr/lib/go/src/runtime/proc.go:201
runtime.goexit
    /usr/lib/go/src/runtime/asm_amd64.s:1333

下面是一個小例子。假設你有一個 AuthInfo 結構體。

Copy
// AuthInfo is authentication information
type AuthInfo struct {
    Login  string // Login user
    ACL    uint   // ACL bitmask
    APIKey string // API key
}

你希望此結構被列印時能夠隱藏 APIKey 的值。你可以使用 ****** 來代替這個 APIKey 的值。

首先我們通過 fmt.Stringer 實現。

Copy
// String implements Stringer interface
func (ai *AuthInfo) String() string {
    key := ai.APIKey
    if key != "" {
        key = keyMask
    }
    return fmt.Sprintf("Login:%s, ACL:%08b, APIKey: %s", ai.Login, ai.ACL, key)
}

現在 fmt.Formatter 獲取到了佔位符的 fmt.State 和符文。fmt.State 實現了 io.Writer 介面,使得你可以直接寫入它。

想要了解結構體中所有的可用欄位,可以使用 reflect 包。這將確保你的程式碼即使在 AuthInfo 欄位更改後也能正常工作。

Copy
var authInfoFields []string

func INIt() {
    typ := reflect.TypeOf(AuthInfo{})
    authInfoFields = make([]string, typ.NumField())
    for i := 0; i < typ.NumField(); i++ {
        authInfoFields[i] = typ.Field(i).Name
    }
    sort.Strings(authInfoFields) // People are better with sorted data
}

現在你就可以實現 fmt.Formatter 介面了。

Copy
// Format implements fmt.Formatter
func (ai *AuthInfo) Format(state fmt.State, verb rune) {
    switch verb {
    case 's', 'q':
        val := ai.String()
        if verb == 'q' {
            val = fmt.Sprintf("%q", val)
        }
        fmt.Fprint(state, val)
    case 'v':
        if state.Flag('#') {
            // Emit type before
            fmt.Fprintf(state, "%T", ai)
        }
        fmt.Fprint(state, "{")
        val := reflect.ValueOf(*ai)
        for i, name := range authInfoFields {
            if state.Flag('#') || state.Flag('+') {
                fmt.Fprintf(state, "%s:", name)
            }
            fld := val.FieldByName(name)
            if name == "APIKey" && fld.Len() > 0 {
                fmt.Fprint(state, keyMask)
            } else {
                fmt.Fprint(state, fld)
            }
            if i < len(authInfoFields)-1 {
                fmt.Fprint(state, " ")
            }
        }
        fmt.Fprint(state, "}")
    }
}

現在讓我們來試試看結果:

Copy
ai := &AuthInfo{
    Login:  "daffy",
    ACL:    ReadACL | WriteACL,
    APIKey: "duck season",
}
fmt.Println(ai.String())
fmt.Printf("ai %%s: %s\n", ai)
fmt.Printf("ai %%q: %q\n", ai)
fmt.Printf("ai %%v: %v\n", ai)
fmt.Printf("ai %%+v: %+v\n", ai)
fmt.Printf("ai %%#v: %#v\n", ai)

這樣這樣打印出:

Copy
Login:daffy, ACL:00000011, APIKey: *****
ai %s: Login:daffy, ACL:00000011, APIKey: *****
ai %q: "Login:daffy, ACL:00000011, APIKey: *****"
ai %v: {3 ***** daffy}
ai %+v: {ACL:3 APIKey:***** Login:daffy}
ai %#v: *main.AuthInfo{ACL:3 APIKey:***** Login:daffy}