龙空技术网

站长带你分析:println 和 fmt.Println 的“诡异”问题

Go语言中文网 625

前言:

今天你们对“println输出数组”可能比较讲究,各位老铁们都想要分析一些“println输出数组”的相关资讯。那么小编在网上收集了一些对于“println输出数组””的相关知识,希望各位老铁们能喜欢,各位老铁们快快来了解一下吧!

在 Go 语言中文网微信群中,一个群友贴了如下代码:

package mainimport (   "fmt")func main() {   done := make(chan int)   go func() {      println("你好")      done <-1   }()   m := <-done   fmt.Println(m)}

在线运行: (细心的你会发现,“你好”是红色的,“1”是黑色)

他给出的结果是:有时候 “你好” 先输出,有时候 1 先输出,顺序不定。

结果群里其他人一试验,发现顺序一直是固定的,即:

你好

1

原来他使用的是 Goland,如果在终端执行,顺序并不会随机,如果你有 Goland,可以试试。

接着大家讨论,引出了一个小知识点。

println 和 fmt.Println 的区别

println 是 builtin 包提供,语言内置,而 fmt.Println 来自标准库。从 println 函数的注释中还可以了解到更多的信息:

// The println built-in function formats its arguments in an// implementation-specific way and writes the result to standard error.// Spaces are always added between arguments and a newline is appended.// Println is useful for bootstrapping and debugging; it is not guaranteed// to stay in the language.

fmt.Println 输出到标准输出(os.Stdout),而 println 输出至标准错误(os.Stderr)。而且 println 在参数和换行都会追加空格。

这里要强调一点:println 主要用于程序启动和调试,语言内部实现主要用它。但并不保证未来会一直有这个函数。所以,建议还是老老实实用 fmt 中的打印函数吧。

引用 go101 中的一个问答:内置的`print`和`println`函数与`fmt`和`log`标准库包中相应的打印函数有什么区别?[1]

内置的print/println函数总是写入标准错误。fmt标准包里的打印函数总是写入标准输出。log标准包里的打印函数会默认写入标准错误,然而也可以通过log.SetOutput函数来配置。内置print/println函数的调用不能接受数组和结构体参数。对于组合类型的参数,内置的print/println函数将输出参数的底层值部的地址,而fmt和log标准库包中的打印函数将输出参数的字面值。目前(Go 1.14),对于标准编译器,调用内置的print/println函数不会使调用参数引用的值逃逸到堆上,而fmt和log标准库包中的的打印函数将使调用参数引用的值逃逸到堆上。如果一个实参有String() string或Error() string方法,那么fmt和log标准库包里的打印函数在打印参数时会调用这两个方法,而内置的print/println函数则会忽略参数的这些方法。内置的print/println函数不保证在未来的Go版本中继续存在。缓冲问题

因为涉及到不同的输出终端,自然涉及到一个问题:缓冲。对 C 有所了解的都知道,基于流的 I/O 提供了 3 种缓冲:

全缓冲:直到缓冲区被填满,才调用系统 I/O 函数。对于读操作来说,直到读入的内容的字节数等于缓冲区大小或者文件已经到达结尾,才进行实际的 I/O 操作,将外存文件内容读入缓冲区;对于写操作来说,直到缓冲区被填满,才进行实际的 I/O 操作,缓冲区内容写到外存文件中。磁盘文件通常是全缓冲的。行缓冲:直到遇到换行符 ‘\n’,才调用系统 I/O 库函数。对于读操作来说,遇到换行符 ‘\n’ 才进行 I/O 操作,将所读内容读入缓冲区;对于写操作来说,遇到换行符 ‘\n’ 才进行 I/O 操作,将缓冲区内容写到外存中。由于缓冲区的大小是有限的,所以当缓冲区被填满时,即使没有遇到换行符‘\n’,也同样会进行实际的 I/O 操作。标准输入 stdin 和标准输出 stdout 默认都是行缓冲的。无缓冲:没有缓冲区,数据会立即读入或者输出到外存文件和设备上。标准错误 stderr 是无缓冲的,这样保证错误提示和输出能够及时反馈给用户,供用户排除错误。

可见,C 中基于流的 I/O,标准输出(stdout)和标准错误(stderr)缓冲方式是不一样的。

如果 Go 中也是如此,那么我们可以改造上面的代码进行试验(更换 println 和 fmt.Println 的位置,以及使用 print 和 fmt.Print),你会发现,在终端输出顺序永远是固定的。也就是说,Go 中的 os.Stdout 并不是行缓冲的。在 golang-nuts 讨论组中有人问了这个问题:os.Stdout is not buffered ?[2],官方给的回答是,os.Stdout 是无缓冲的,因为它的类型实际上是 *os.File,很显然 *os.File 是无缓冲的。

既然 os.Stdout 和 os.Stderr 都是无缓冲的,那么终端的输出顺序是固定的也就不足为奇了。我们自己实现一个行缓冲的 os.Stdout,来验证它。

package mainimport ( "bufio" "bytes" "fmt" "io" "os")func main() { writer := NewLineBufferedWriter(os.Stdout) defer writer.Flush() done := make(chan int) go func() {  fmt.Fprint(writer, "你好")  done <- 1 }() m := <-done println(m)}type LineBufferedWriter struct { *bufio.Writer}func NewLineBufferedWriter(w io.Writer) *LineBufferedWriter { return &LineBufferedWriter{  Writer: bufio.NewWriter(w), }}func (w *LineBufferedWriter) Write(p []byte) (n int, err error) { n, err = w.Writer.Write(p) if err != nil {  return n, err } if bytes.Contains(p, []byte{'\n'}) {  w.Flush() } return n, err}

以上代码,因为有行缓冲,永远输出:

1

你好

你扯吧?我这里怎么还是顺序不确定。

好吧,你依然用 Goland 运行的吧。

总结

通过以上的分析,我们可以得出如下结论:

Go 语言中的 标准输入、标准输出和标准错误 都是无缓冲的。这点和 C 语言不一样;当你遇到“诡异”问题时,通过终端方式验证下,很多时候可能编辑器,特别是 IDE 做了一些隐藏的事情。从上面的分析可以肯定,Goland 在处理标准输出和标准错误时做了额外的一些事情,具体是什么不得而知;Go 中如果需要缓冲,请使用 bufio 包,特殊的需求,可以基于它进行扩展。参考资料

[1]

内置的print和println函数与fmt和log标准库包中相应的打印函数有什么区别?:

[2]

os.Stdout is not buffered ?:

标签: #println输出数组