15
8

More than 1 year has passed since last update.

【Go】log.Printとlog.Printlnの違い (実はどちらも改行される)

Last updated at Posted at 2023-09-14

はじめに

こんにちは、kenです。お仕事ではよくGoを書きます。
突然ですが、みなさんはlog.Printlog.Printlnの違いを即座に答えられますでしょうか。

「log.Printは末尾に改行が入らなくて、log.Printlnは改行が入るんでしょ…」

私もそう思っていたのですが、実はlog.Printlog.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.Printlog.Printlnも末尾に改行が差し込まれていますね。一方でfmt.Printは末尾に改行が入っていません。
つまり「ln」のsuffixがある関数なら末尾に改行が差し込まれるという理解はおおむねあっているのですが、log.Printlog.Printlnにはその理解は通用しないということですね…。

であるならば、log.Printlog.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.Printlog.Printlnの違いはfmt.Sprntfmt.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.Sprintfmt.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つの違いはdoPrintdoPrintlnの実装を読めばスッキリとわかります。階層が深くなってきましたが、さらにこの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.Printlog.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.Printlog.Printlnの違いがはっきりと分かりました。ここまでの流れを最後にまとめておくと

  • log.Printlog.Printlnはオペランドとの間にいつスペースが追加されるかのタイミングが違う
  • log.Printは文字列でないものの間にスペースが入る
  • log.Printlnはすべてのオペランドとの間にスペースが入る
  • log.Printlog.Printlnも末尾に改行がない場合は改行が追加される
  • 改行が追加される実装は双方から呼び出されているstd.Outputの中に書かれている

さいごに

log.Printでも改行が挿入されるのにはびっくりでしたね。ちなみにこの話題は golang-nutsのメーリングリストでも話題に上がっており、スレッド主はバグではないかと報告していました。(まあそう思いますよね、、)
内部実装見ていくことで疑問が解決出来て良かったです。ここまで読んでいただきありがとうございました。間違いなどありましたらコメントにてご指摘ください。

15
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
8