4.12. 打印输出

前面例子中涉及到的打印都比较简单。在这一节中,我们将要讨论Go语言格式化输出的功能。

我们已经用过"fmt"包中的"Printf"和"Fprintf"等输出函数。"fmt"包中的"Printf"函数的 完整说明如下:

          Printf(format string, v ...) (n int, errno os.Error)

其中"..."表示数目可变参数,和C语言中"stdarg.h"中的宏类似。不过Go中,可变参数是通道 一个空接口("interface {}")和反射(reflection)库实现的。反射特性可以帮助"Printf" 函数很好的获取参数的详细特征。

在C语言中,printf函数的要格式化的参数类型必须和格式化字符串中的标志一致。不过在Go语言中, 这些细节都被简化了。我们不再需要"%llud"之类的标志,只用"%d"表示要输出一个整数。至于对应 参数的实际类型,"Printf"可以通过反射获取。例如:

  10        var u64 uint64 = 1<<64-1
  11        fmt.Printf("%d %d\n", u64, int64(u64))

输出

          18446744073709551615 -1

最简单的方法是用"%v"标志,它可以以适当的格式输出任意的类型(包括数组和结构)。下面的程序,

  14        type T struct {
  15            a int
  16            b string
  17        }
  18        t := T{77, "Sunset Strip"}
  19        a := []int{1, 2, 3, 4}
  20        fmt.Printf("%v %v %v\n", u64, t, a)

将输出:

          18446744073709551615 {77 Sunset Strip} [1 2 3 4]

如果是使用"Print"或"Println"函数的话,甚至不需要格式化字符串。这些函数会针对数据类型 自动作转换。"Print"函数默认将每个参数以"%v"格式输出,"Println"函数则是在"Print"函数 的输出基础上增加一个换行。一下两种输出方式和前面的输出结果是一致的。

  21        fmt.Print(u64, " ", t, " ", a, "\n")
  22        fmt.Println(u64, t, a)

如果要用"Printf"或"Print"函数输出似有的结构类型,之需要为该结构实现一个"String()"方法, 返回相应的字符串就可以了。打印函数会先检测该类型是否实现了"String()"方法,如果实现了则以 该方法返回字符串作为输出。下面是一个简单的例子。

  09    type testType struct {
  10        a int
  11        b string
  12    }

  14    func (t *testType) String() string {
  15        return fmt.Sprint(t.a) + " " + t.b
  16    }

  18    func main() {
  19        t := &testType{77, "Sunset Strip"}
  20        fmt.Println(t)
  21    }

因为 *testType 类型有 String() 方法,因此格式化函数用它作为输出结果:

          77 Sunset Strip

前面的例子中,"String()"方法用到了"Sprint"(从字面意思可以猜测函数将返回一个字符串) 作为格式化的基础函数。在Go中,我们可以递归使用"fmt"库中的函数来为格式化服务。

"Printf"函数的另一种输出是"%T"格式,它输出的内容更加详细,可以作为调试信息用。

自己实现一个功能完备,可以输出各种格式和精度的函数是可能的。不过这不是该教程的重点,大家 可以把它当作一个课后练习。

读者可能有疑问,"Printf"函数是如何知道变量是否有"String()"函数实现的。实际上,我们 需要先将变量转换为Stringer接口类型,如果转换成功则表示有"String()"方法。下面是一个 演示的例子:

          type Stringer interface {
                  String() string
          }

          s, ok := v.(Stringer);  // Test whether v implements "String()"
          if ok {
                  result = s.String()
          } else {
                  result = defaultOutput(v)
          }

这里用到了类型断言("v.(Stringer)"),用来判断变量"v"是否可以满足"Stringer"接口。 如果满足,"s"将对应转换后的Stringer接口类型并且"ok"被设置为"true"。然后我们通过"s", 以Stringer接口的方式调用String()函数。如果不满足该接口特征,"ok"将被设置为false。

"Stringer"接口的命名通常是在接口方法的名字后面加e?r后缀,这里是"String+er"。

Go中的打印函数,除了"Printf"和"Sprintf"等之外,还有一个"Fprintf"函数。不过"Fprintf"函数和 的第一个参数并不是一个文件,而是一个在"io"库中定义的接口类型:

          type Writer interface {
                  Write(p []byte) (n int, err os.Error);
          }

这里的接口也是采用类似的命名习惯,类型的接口还有"io.Reader"和"io.ReadWriter?"等。 在调用"Fprintf"函数时,可以用实现了"Write"方法的任意类型变量作为参数,例如文件、网络、管道等等。