Go語言學習 二十四 打印

本文最初發表在我的個人博客,查看原文,獲得更好的閱讀體驗


Go中的格式化打印使用類似C的printf系列的風格,但功能更爲豐富和通用。這些函數位於fmt包中,並具有大寫名稱:fmt.Printffmt.Fprintffmt.Sprintf 等等。字符串函數(Sprintf等)返回一個字符串,而不是填充提供的緩衝區。

fmt包中打印函數如下:

func Fprint(w io.Writer, a ...interface{}) (n int, err error)
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error)
func Fprintln(w io.Writer, a ...interface{}) (n int, err error)

func Print(a ...interface{}) (n int, err error)
func Printf(format string, a ...interface{}) (n int, err error)
func Println(a ...interface{}) (n int, err error)

func Sprint(a ...interface{}) string
func Sprintf(format string, a ...interface{}) string
func Sprintln(a ...interface{}) string

一 格式字符串中的佔位符

對於以f結尾的格式化打印函數FprintfPrintfSprintf可以接受一個格式字符串,其中格式字符串中有相應的佔位符,不同的佔位符格式效果不一樣。

1.1 常規格式化

%v	默認格式。當打印結構類型時,帶加號的標識(%+v)會添加字段名稱
%#v	值的Go語法表示形式
%T	值類型的Go語法表示形式
%%	百分號標識字面量,無需提供值

示例:

package main

import (
	"fmt"
)

func main() {
	t := &T{7, -2.35, "abc\tdef"}

	fmt.Printf("%v\n", t)  // &{7 -2.35 abc	def}
	fmt.Printf("%+v\n", t) // &{a:7 b:-2.35 c:abc	def}
	fmt.Printf("%#v\n", t) // &main.T{a:7, b:-2.35, c:"abc\tdef"}
	fmt.Printf("%T\n", t)  // *main.T
	fmt.Printf("20%%\n")   // 20%
}

type T struct {
	a int
	b float64
	c string
}

1.2 布爾格式化

%t  輸出布爾變量值 true 或 false

1.3 整數格式化

%b	二進制形式
%c	相應的Unicode碼點表示的字符
%d	十進制形式
%o	八進制形式
%q	使用Go語法安全轉義的單引號字符字面量
%x	十六進制形式,a-f爲小寫
%X	十六進制形式,A-F爲大寫
%U	Unicode格式: U+1234; 等價於 "U+%04X"

示例:

a := 255

fmt.Printf("%b\n", a) // 11111111
fmt.Printf("%c\n", a) // ÿ
fmt.Printf("%d\n", a) // 255
fmt.Printf("%o\n", a) // 377
fmt.Printf("%q\n", a) // 'ÿ'
fmt.Printf("%x\n", a) // ff
fmt.Printf("%X\n", a) // FF
fmt.Printf("%U\n", a) // U+00FF

1.4 浮點數和複數格式化

%b	指數爲二的冪的無小數科學計數法,與strconv.FormatFloat函數的'b'格式一致。例如:6755399441055744p-52
%e	科學計數法(e小寫), 如 -1.234456e+78
%E	科學計數法(E大寫), 如 -1.234456E+78
%f	無指數小數,如 123.456
%F	同 %f
%g	用%e 表示大指數,否則用 %f 。 
%G	用%E 表示大指數,否則用 %F 。 

6755399441055744p-52 表示 6755399441055744 * 2-52,即:float64的1.5

關於精度的討論,請參考下面1.8小節。

示例:

b := 1234567.891

fmt.Printf("%b\n", b) // 5302428716536693p-32
fmt.Printf("%e\n", b) // 1.234568e+06
fmt.Printf("%E\n", b) // 1.234568E+06
fmt.Printf("%f\n", b) // 1234567.891000
fmt.Printf("%F\n", b) // 1234567.891000
fmt.Printf("%g\n", b) // 1.234567891e+06
fmt.Printf("%G\n", b) // 1.234567891E+06

1.5 字符串和字節切片格式化

%s	字符串或字節切片的未解釋字節。
%q	Go語法安全轉義的雙引號字符串。
%x	十六進制形式,小寫,每字節兩個字符。
%X	十六進制形式,大寫,每字節兩個字符

示例:

s1 := `hello \nworld`
s2 := "hello \nworld"

fmt.Printf("%s\n", s1) // hello \nworld
fmt.Printf("%s\n", s2) // hello
                       // world // 注意這裏換行了

fmt.Printf("%q\n", s1) // "hello \\nworld"
fmt.Printf("%q\n", s2) // "hello \nworld"

fmt.Printf("%x\n", s1) // 68656c6c6f205c6e776f726c64
fmt.Printf("%x\n", s2) // 68656c6c6f200a776f726c64

fmt.Printf("%X\n", s1) // 68656C6C6F205C6E776F726C64
fmt.Printf("%X\n", s2) // 68656C6C6F200A776F726C64

1.6 切片格式化

%p	第0個元素的地址的16進製表示形式,前導爲0x。

示例:

s := make([]int, 2, 16)
fmt.Printf("%p\n", s) // 0x432080

1.7 指針格式化

%p	16進製表示形式,前導爲0x。
%b, %d, %o, %x 和 %X 同樣可以用來格式化指針,就像對待整數一樣。

對於%v表示的默認格式,不同類型相當於:

bool:                    %t
int, int8 等:            %d
uint, uint8 等:          %d, 如果是%#v,則爲%#x
float32, complex64, 等:  %g
string:                  %s
chan:                    %p
pointer:                 %p

對於複合對象,使用以下規則遞歸打印:

struct:             {field0 field1 ...}
array, slice:       [elem0 elem1 ...]
maps:               map[key1:value1 key2:value2 ...]
以上類型的指針:       &{}, &[], &map[]

1.8 寬度和精度

另外,還可以指定寬度和精度,格式爲%w.p佔位符,其中:w爲指定的寬度,.p爲指定的精度,注意p前面的點號,並不是所有的佔位符都可以使用精度。如果實際寬度小於指定的寬度,則在前面補空格;如果實際寬度大於指定寬度,以實際寬度爲準;如果缺省,則默認寬度是表示該值所必需的寬度,對於小數部分,默認寬度總是6。如果沒有點號.,則使用默認精度;如果點號後沒有數字p,則精度爲0。精度在舍入時爲四捨五入。寬度和精度均爲十進制數字,且都是可選的。
示例:

b1 := 123456789.81234567
b2 := 12345678907.81
b3 := 12345678907.82

fmt.Printf("%f\n", b1)     // "123456789.812346"   - 默認寬度和精度
fmt.Printf("%f\n", b2)     // "12345678907.809999" - 默認寬度和精度
fmt.Printf("%f\n", b3)     // "12345678907.820000" - 默認寬度和精度
fmt.Println()              //
fmt.Printf("%20f\n", b1)   // "    123456789.812346" - 寬度20,默認精度
fmt.Printf("%.2f\n", b1)   // "123456789.81"         - 默認寬度,精度2
fmt.Printf("%16.2f\n", b1) // "    123456789.81"     - 寬度16,精度2
fmt.Printf("%9.f\n", b1)   // "123456790"            - 寬度9,精度0

注意第6行的結果並不能精確表示實際值
另外,上述示例中的註釋爲了體現差異增加了兩端的雙引號,實際不會輸出,下同

1.9 其他標記

+	總是爲數字值打印符號;對於%q (%+q),只保證ASCII字符的輸出。
-	右側填充空格,而不是左側(左對齊)
#	可選格式: 對於八進制(%#o)添加前導0;十六進制(%#x)添加前導0x,十六進制(%#X)添加前導0X;
    對於%p (%#p)則抑制前導0x;
    對於%q,如果strconv.CanBackquote返回true,則打印原始字符串(反引號字符串);
	對於%e, %E, %f, %F, %g 和 %G總是打印一個小數點;
	對於%g 和 %G,不會移除結尾的0;
	如果字符'x'可用%U (%#U)打印,則會輸出例如 U+0078 'x'
' '	(空格) 爲數字中省略的符號保留一個空格(% d);
    以十六進制打印字符串或切片時(% x, % X),在字節之間插入空格
0	使用0而不是空格填充寬度;對於數字,則是在符號後填充。

示例:

n1 := 123
b1 := 123456789.81234567
s1 := 'x'
s2 := "ABC"

fmt.Printf("%+d\n", n1)    // "+123"
fmt.Printf("%+d\n", -n1)   // "-123"
fmt.Println()              //
fmt.Printf("%d\n", n1)     // "123"
fmt.Printf("%d\n", -n1)    // "-123"
fmt.Printf("% d\n", n1)    // " 123"
fmt.Printf("% d\n", -n1)   // "-123"
fmt.Println()              //
fmt.Printf("%020f\n", -b1) // "-000123456789.812346"
fmt.Printf("%020f\n", b1)  // "0000123456789.812346"
fmt.Printf("%#9.f\n", b1)  // "123456790."
fmt.Println()              //
fmt.Printf("%U\n", s1)     // "U+0078"
fmt.Printf("%#U\n", s1)    // "U+0078 'x'"
fmt.Printf("%5s\n", s2)    // "  ABC"
fmt.Printf("%-5s\n", s2)   // "ABC  "
fmt.Printf("%05s\n", s2)   // "00ABC"

二 格式化打印

你不需要提供格式字符串。對於PrintfFprintfSprintf中的每一個,還有另外的函數與之對應,比如PrintPrintln,這些函數不使用格式字符串,而是爲每個參數生成默認格式。Println版本還在參數之間插入一個空格,並在輸出中附加一個換行符,而Print版本只在兩邊的操作數都不是字符串時才添加空格。下面的例子輸出的結果一樣:

fmt.Printf("Hello %d\n", 23)
fmt.Fprint(os.Stdout, "Hello ", 23, "\n")
fmt.Println("Hello", 23)
fmt.Println(fmt.Sprint("Hello ", 23))

格式化打印函數如fmt.Fprint等,將任何實現了io.Writer接口的對象作爲第一個參數,像變量os.Stdoutos.Stderr

接下來的事情與C不同,首先,像%d等數字格式不接受符號或大小標識,相反,打印例程會使用參數的類型來決定這些屬性。

var x uint64 = 1<<64 - 1
fmt.Printf("%d %x; %d %x\n", x, x, int64(x), int64(x))

會打印出:

18446744073709551615 ffffffffffffffff; -1 -1

如果只是想要默認轉換,例如decimalinteger,可以使用%v格式,結果正是PrintPrintln將產生的結果。而且,該格式可以打印任何值,甚至是數組、切片,結構和映射。
例如打印映射的語句:

var color = map[string]string{
	"red":    "紅",
	"orange": "橙",
	"yellow": "黃",
	"green":  "綠", // 注意每行結尾的逗號不能少
}

fmt.Printf("%v\n", color)
fmt.Println(color)

會打印出:

map[green:綠 orange:橙 red:紅 yellow:黃]
map[green:綠 orange:橙 red:紅 yellow:黃]

對於映射,輸出的key的順序是不固定的。對於結構類型,改版的%+v格式使用它們的名稱註解其中的字段,對於任何值,%#v替換格式以完整的Go語法打印。

type T struct {
    a int
    b float64
    c string
}
t := &T{ 7, -2.35, "abc\tdef" }
fmt.Printf("%v\n", t)
fmt.Printf("%+v\n", t)
fmt.Printf("%#v\n", t)
fmt.Printf("%#v\n", color)

會打印出:

&{7 -2.35 abc   def}
&{a:7 b:-2.35 c:abc     def}
&main.T{a:7, b:-2.35, c:"abc\tdef"}
map[string]string{"green":"綠", "orange":"橙", "red":"紅", "yellow":"黃"}

注意&符號。當應用於string[]byte類型的值時,帶引號的字符串格式也可以通過%q獲得。如果可能,備用格式%#q將使用反引號。(%q也適用於整數和符文,會輸出單引號的符號常量)。此外,%x適用於字符串,字節數組和字節切片以及整數,生成長十六進制字符串,中間有個空格的% x格式會在字節之間放置空格。

另一個方便的格式是%T,它打印一個值的類型:

fmt.Printf("%T\n", color)

會打印出:

map[string]string

如果要控制自定義類型的默認格式,只需要在類型中定義一個String() string簽名的方法即可,例如:

func (t *T) String() string {
    return fmt.Sprintf("%d/%g/%q", t.a, t.b, t.c)
}
fmt.Printf("%v\n", t)

輸出:

7/-2.35/"abc\tdef"

如果你需要像打印類型T的指針那樣打印T的值,String的接收器必須是一個值類型,這個例子使用了一個指針,因爲對於結構類型更有效和慣用。

我們的String方法能夠調用Sprintf,是因爲打印例程完全是可重入的,並且以這種方式封裝。但是,要理解這種方法有一個重要的細節:請勿通過調用Sprintf來構造String方法,因爲它會無限遞歸String方法。如果Sprintf調用嘗試直接以字符串形式打印接收器,而字符串又會再次調用該方法,就會發生這種情況,這是一個常見且容易犯的錯誤。例如以下的錯誤示例:

type MyString string

func (m MyString) String() string {
    return fmt.Sprintf("MyString=%s", m) // Error: will recur forever.
}

很容易修復這個問題:將參數轉換爲基本字符串類型,該類型沒有這個方法。

type MyString string
func (m MyString) String() string {
    return fmt.Sprintf("MyString=%s", string(m)) // OK: note conversion.
}

另一種解決辦法是使用非字符串格式標記,例如:

type ByteSize float64

const (
    _           = iota // ignore first value by assigning to blank identifier
    KB ByteSize = 1 << (10 * iota)
    MB
    GB
    TB
    PB
    EB
    ZB
    YB
)

func (b ByteSize) String() string {
    switch {
    case b >= YB:
        return fmt.Sprintf("%.2fYB", b/YB)
    case b >= ZB:
        return fmt.Sprintf("%.2fZB", b/ZB)
    case b >= EB:
        return fmt.Sprintf("%.2fEB", b/EB)
    case b >= PB:
        return fmt.Sprintf("%.2fPB", b/PB)
    case b >= TB:
        return fmt.Sprintf("%.2fTB", b/TB)
    case b >= GB:
        return fmt.Sprintf("%.2fGB", b/GB)
    case b >= MB:
        return fmt.Sprintf("%.2fMB", b/MB)
    case b >= KB:
        return fmt.Sprintf("%.2fKB", b/KB)
    }
    return fmt.Sprintf("%.2fB", b)
}

在這裏用Sprintf實現ByteSizeString方法很安全(不會無限遞歸),這倒不是因爲類型轉換,而是它以%f調用了Sprintf,它並不是一種字符串格式:Sprintf只會在它需要字符串時才調用String方法,而%f需要一個浮點數值。

另一種打印技術是將打印例程的參數直接傳遞給另一個這樣的例程。Printf的簽名使用...interface{}類型作爲最終參數,以指定在格式化之後可以出現任意數量的參數(任意類型):

func Printf(format string, v ...interface{}) (n int, err error) {

在函數Printf中,v相當於[]interface類型的變量,但是如果它被傳遞給另一個可變參數函數,它就像一個常規的參數列表。這是我們上面使用的函數的log.Println的實現。它將其參數直接傳遞給fmt.Sprintln以進行實際格式化。

// Println prints to the standard logger in the manner of fmt.Println.
func Println(v ...interface{}) {
    std.Output(2, fmt.Sprintln(v...))  // Output takes parameters (int, string)
}

我們在對嵌套的Sprintln調用中的v後邊加上...告訴編譯器將v視作參數列表,否則它只會將v作爲單個切片參數傳遞。

順便說一下,...參數可以是特定類型,例如用於選擇最小整數列表min函數的... int

func Min(a ...int) int {
    min := int(^uint(0) >> 1)  // largest int
    for _, i := range a {
        if i < min {
            min = i
        }
    }
    return min
}

參考:
https://golang.org/doc/effective_go.html#printing
https://golang.org/pkg/fmt

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章