はじめに
こんにちは、kenです。お仕事ではよくGoを書きます。
突然ですが、みなさんはlog.Print
とlog.Println
の違いを即座に答えられますでしょうか。
「log.Printは末尾に改行が入らなくて、log.Printlnは改行が入るんでしょ…」
私もそう思っていたのですが、実はlog.Print
とlog.Println
はどちらも末尾に改行が差し込まれます。
では一体この2つは何が違うのでしょうか。この記事はそんな小ネタの紹介をしたいと思います。
ほんとにlog.Print
にも末尾に改行が入るの?
「log.Println
の『ln
』は末尾に改行が入るという意味の『line』から由来しているのでは…?」
そう信じて疑わない人もいるはずなので実例をお見せしようと思います。
次のコードを実行したときには何が出力されるでしょうか。
package main
import (
"fmt"
"log"
)
func main() {
fmt.Println("logパッケージ")
log.Print("Hello World!")
log.Println("Hello World!")
fmt.Println("-------------------")
fmt.Println("fmtパッケージ")
fmt.Print("Hello World!")
fmt.Println("Hello World!")
}
答えは以下のとおりです。
$ go run .
logパッケージ
2023/09/09 20:06:48 Hello World!
2023/09/09 20:06:48 Hello World!
-------------------
fmtパッケージ
Hello World!Hello World!
たしかにlog.Print
もlog.Println
も末尾に改行が差し込まれていますね。一方でfmt.Print
は末尾に改行が入っていません。
つまり「ln」のsuffixがある関数なら末尾に改行が差し込まれるという理解はおおむねあっているのですが、log.Print
とlog.Println
にはその理解は通用しないということですね…。
であるならば、log.Print
とlog.Println
は一体なにが違うんでしょうか?
log.Printとlog.Printlnの違い
結論からいうと、log.Print
は オペランドが文字列でないものの間にはスペースが追加され、log.Println
はオペランドとの間には常にスペースが追加される、という違いがあります。
具体的なコードで確認してみましょう。
package main
import "log"
func main() {
log.Print("A", 1, 2, "B", 3, "C", "D")
log.Println("A", 1, 2, "B", 3, "C", "D")
}
このコードの実行結果は以下のようになります。
2023/09/09 20:15:01 A1 2B3CD
2023/09/09 20:15:01 A 1 2 B 3 C D
確かに、log.Print
の方では両方ともstringではない「1」と「2」の間にのみスペースが追加されていて、log.Println
の方ではすべてのオペランドの間にスペースが追加されています。
なぜこのような違いが生まれるのか内部実装を除いて確かめてみましょう。
logパッケージの中身を一部引用します。
// Print calls Output to print to the standard logger.
// Arguments are handled in the manner of fmt.Print.
func Print(v ...any) {
if std.isDiscard.Load() {
return
}
std.Output(2, fmt.Sprint(v...))
}
// Println calls Output to print to the standard logger.
// Arguments are handled in the manner of fmt.Println.
func Println(v ...any) {
if std.isDiscard.Load() {
return
}
std.Output(2, fmt.Sprintln(v...))
}
これをみるとlog.Print
とlog.Println
の違いはfmt.Sprnt
とfmt.Sprintln
の違いによって引き起こされているようです。次はこの2つの内部実装を確かめるためにfmtパッケージの中身を見に行きます。(再び引用します)
// Sprint formats using the default formats for its operands and returns the resulting string.
// Spaces are added between operands when neither is a string.
func Sprint(a ...any) string {
p := newPrinter()
p.doPrint(a)
s := string(p.buf)
p.free()
return s
}
// Sprintln formats using the default formats for its operands and returns the resulting string.
// Spaces are always added between operands and a newline is appended.
func Sprintln(a ...any) string {
p := newPrinter()
p.doPrintln(a)
s := string(p.buf)
p.free()
return s
}
このfmt.Sprint
とfmt.Sprintln
の各説明を読んでみると、先程結論として書いたことがそのまま書かれていると思います。
それぞれの説明を日本語に訳すと次のようになります。
// Sprint formats using the default formats for its operands and returns the resulting string.
// Spaces are added between operands when neither is a string.
(筆者訳:Sprintは、オペランドにデフォルトの書式を使用して書式を設定し、結果の文字列を返します。どちらも文字列でない場合は、オペランド間にスペースが追加されます。)
// Sprintln formats using the default formats for its operands and returns the resulting string.
// Spaces are always added between operands and a newline is appended.
(筆者訳:Sprintlnは、オペランドにデフォルトの書式を使用して書式を設定し、結果の文字列を返します。オペランド間には常にスペースが追加され、改行が付加されます。)
上に書いた2つの違いはdoPrint
とdoPrintln
の実装を読めばスッキリとわかります。階層が深くなってきましたが、さらにこの2つの実装を確かめにいきましょう。(またまた引用します)
func (p *pp) doPrint(a []any) {
prevString := false
for argNum, arg := range a {
isString := arg != nil && reflect.TypeOf(arg).Kind() == reflect.String
// Add a space between two non-string arguments.
if argNum > 0 && !isString && !prevString {
p.buf.writeByte(' ')
}
p.printArg(arg, 'v')
prevString = isString
}
}
func (p *pp) doPrintln(a []any) {
for argNum, arg := range a {
if argNum > 0 {
p.buf.writeByte(' ')
}
p.printArg(arg, 'v')
}
p.buf.writeByte('\n')
}
ふむふむ。たしかにdoPrint
の方はif文の条件である!isString && !prevString
からも分かる通り、文字列でないものの間にのみスペースを挿入する実装になってそうです。
ってあれ!?doPrintln
には最後に改行が追加されてて(p.buf.writeByte('\n')
のところ)、doPrint
では改行を追加するコードが書かれてないじゃん!
やっぱりlog.Print
でも末尾に改行が挿入されるっていうのは嘘なんか??
そう思ったあなた、ご心配なく。実は改行にまつわるコードはlog.Print
とlog.Println
で出てきたstd.Output
の中にも出てきます。
func Print(v ...any) {
if std.isDiscard.Load() {
return
}
std.Output(2, fmt.Sprint(v...)) // ←こいつ
}
func Println(v ...any) {
if std.isDiscard.Load() {
return
}
std.Output(2, fmt.Sprintln(v...)) // ←こいつ
}
std.Output
の中身を覗いてみると(またまた引用(ry)
// Output writes the output for a logging event. The string s contains
// the text to print after the prefix specified by the flags of the
// Logger. A newline is appended if the last character of s is not
// already a newline. Calldepth is used to recover the PC and is
// provided for generality, although at the moment on all pre-defined
// paths it will be 2.
func (l *Logger) Output(calldepth int, s string) error {
now := time.Now() // get this early.
var file string
var line int
l.mu.Lock()
defer l.mu.Unlock()
if l.flag&(Lshortfile|Llongfile) != 0 {
// Release lock while getting caller info - it's expensive.
l.mu.Unlock()
var ok bool
_, file, line, ok = runtime.Caller(calldepth)
if !ok {
file = "???"
line = 0
}
l.mu.Lock()
}
l.buf = l.buf[:0]
l.formatHeader(&l.buf, now, file, line)
l.buf = append(l.buf, s...)
if len(s) == 0 || s[len(s)-1] != '\n' {
l.buf = append(l.buf, '\n')
}
_, err := l.out.Write(l.buf)
return err
}
注目すべきは最後のほうの
if len(s) == 0 || s[len(s)-1] != '\n' {
l.buf = append(l.buf, '\n')
}
の部分です。これは末尾に改行がされていない場合に改行が差し込まれるという実装になっています。
これにより、log.Print
を呼び出したときその内部で呼び出されているfmt.Print
では改行がなされないものの、最後のstd.Output
を通過するときに改行が差し込まれるといった構造になっています。したがって結果的に改行ありの文字列が出力されているんですね。
これでlog.Print
とlog.Println
の違いがはっきりと分かりました。ここまでの流れを最後にまとめておくと
-
log.Print
とlog.Println
はオペランドとの間にいつスペースが追加されるかのタイミングが違う -
log.Print
は文字列でないものの間にスペースが入る -
log.Println
はすべてのオペランドとの間にスペースが入る -
log.Print
もlog.Println
も末尾に改行がない場合は改行が追加される - 改行が追加される実装は双方から呼び出されている
std.Output
の中に書かれている
さいごに
log.Printでも改行が挿入されるのにはびっくりでしたね。ちなみにこの話題は golang-nutsのメーリングリストでも話題に上がっており、スレッド主はバグではないかと報告していました。(まあそう思いますよね、、)
内部実装見ていくことで疑問が解決出来て良かったです。ここまで読んでいただきありがとうございました。間違いなどありましたらコメントにてご指摘ください。