首页 > 技术文章 > 站长带你分析:println 和 fmt.Println 的“诡异”问题

ebuybay 2022-02-15 17:14 原文

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

  package main

  import (

  "fmt"

  )

  func main() {

  done := make(chan int)

  go func() {

  println("你好")

  done <-1

  }()

  m := <-done

  fmt.Println(m)

  }

  在线运行:

  play.studygolang/p/Itvy83BKIIZ (细心的你会发现,“你好”是红色的,“1”是黑色)

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

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

  你好

  1

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

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

  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 操作,缓冲区内容写到外存文件中。磁盘文件通常是全缓冲的。行缓冲:直到遇到换行符 ‘

  ’,才调用系统 I/O 库函数。对于读操作来说,遇到换行符 ‘

  ’ 才进行 I/O 操作,将所读内容读入缓冲区;对于写操作来说,遇到换行符 ‘

  ’ 才进行 I/O 操作,将缓冲区内容写到外存中。由于缓冲区的大小是有限的,所以当缓冲区被填满时,即使没有遇到换行符‘

  ’,也同样会进行实际的 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 main

  import (

  "bufio"

  "bytes"

  "fmt"

  "io"

  "os"

  )

  func main() {

  writer := NewLineBufferedWriter(os.Stdout)

  defer writer.Flush()

  done := make(chan int)

  go func() {

  fmt.Fprint(writer, "你好")

  done

  }()

  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{'

  '}) {

  w.Flush()

  }

  return n, err

  }

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

  1

  你好

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

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

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

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

推荐阅读