#81577·Go

testing: `go test -v` doubles ESC (0x1b) in t.Log output since Go 1.27, corrupting ANSI colors in some terminals

Author: AndrewDeryabinCreated Sep 17, 2026Updated Sep 17, 2026

Go version

go version go1.27.1-X:nodwarf5 linux/arm64

Output of go env in your module/workspace:

AR='ar'
CC='gcc'
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_ENABLED='1'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
CXX='g++'
GCCGO='gccgo'
GO111MODULE=''
GOARCH='arm64'
GOARM64='v8.0'
GOAUTH='netrc'
GOBIN='/home/user/go/bin'
GOCACHE='/home/user/.cache/go-build'
GOCACHEPROG=''
GODEBUG=''
GOENV='/home/user/.config/go/env'
GOEXE=''
GOEXPERIMENT='nodwarf5'
GOFIPS140='off'
GOFLAGS=''
GOGCCFLAGS='-fPIC -pthread -fmessage-length=0 -ffile-prefix-map=/tmp/go-build3411497907=/tmp/go-build -gno-record-gcc-switches'
GOHOSTARCH='arm64'
GOHOSTOS='linux'
GOINSECURE=''
GOMOD='/home/user/esc/go.mod'
GOMODCACHE='/home/user/go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='linux'
GOPACKAGESDRIVER=''
GOPATH='/home/user/go'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/usr/lib/go'
GOSUMDB='sum.golang.org'
GOTELEMETRY='off'
GOTELEMETRYDIR='/home/user/.config/go/telemetry'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/usr/lib/go/pkg/tool/linux_arm64'
GOVCS=''
GOVERSION='go1.27.1-X:nodwarf5'
GOWORK=''
PKG_CONFIG='pkg-config'

What did you do?

Logged a line containing ANSI SGR escape sequences through t.Log and ran the test with go test -v in a terminal. Found through a logging library whose test helper writes the output of a colored log/slog handler through testing.TB.Log: the colors looked right in Apple Terminal but came out corrupted in kitty.

Minimal reproducer:

package escrepro

import (
	"fmt"
	"testing"
)

func TestColor(t *testing.T) {
	t.Log("\x1b[92mgreen via t.Log\x1b[0m")
	fmt.Println("\x1b[92mgreen via fmt.Println\x1b[0m") // comparison: bypasses the testing package
}
go test -count=1 -v . | cat -v      # cat -v shows ESC (0x1b) as ^[

What did you see happen?

With Go 1.27.1 every ESC in the t.Log line is doubled, while the fmt.Println line is intact:

=== RUN   TestColor
    esc_test.go:9: ^[^[[92mgreen via t.Log^[^[[0m
^[[92mgreen via fmt.Println^[[0m
--- PASS: TestColor (0.00s)
PASS
ok  	example.com/escrepro	0.002s

od -c of the t.Log line:

0000000                   e   s   c   _   t   e   s   t   .   g   o   :
0000020   9   :     033 033   [   9   2   m   g   r   e   e   n       v
0000040   i   a       t   .   L   o   g 033 033   [   0   m  \n

With Go 1.26.0 (GOTOOLCHAIN=go1.26.0 go test -count=1 -v . | cat -v) the bytes come out as logged:

=== RUN   TestColor
    esc_test.go:9: ^[[92mgreen via t.Log^[[0m
^[[92mgreen via fmt.Println^[[0m
--- PASS: TestColor (0.00s)

Further observations:

  • Running the compiled test binary directly (go test -c -o esc.test . && ./esc.test -test.v | cat -v) shows the same doubled ESC, so it is the testing package, not cmd/go.
  • go test -json is fine: test2json strips the escapes and the JSON Output field contains \u001b[92m once. Only the human-readable -v stream is affected.
  • t.Error, t.Fatal and t.Output() go through the same code path as t.Log.
  • Terminals differ in how they cope. Apple Terminal, like xterm and other DEC-style parsers, treats an ESC arriving inside an escape sequence as the start of a new one, so ESC ESC [92m still renders green and the corruption goes unnoticed. kitty does not tolerate the doubled ESC and the colored lines come out corrupted:
  • Anything that consumes the raw -v stream sees corrupted data, e.g. a test running a child go test -v on a pseudo-terminal and asserting on its output.

Cause: (*outputWriter).writeLine in src/testing/testing.go calls escapeMarkers(b) whenever the test runs in chatty (-v) mode, regardless of chatty.json. escapeMarkers inserts ^[ before each of ^V, ^O, ^N and ^[.

This arrived in Go 1.27 with CL 751940 ("testing: escapes framing markers", for #62728), whose stated purpose is to keep such bytes from corrupting "the test2json parse of the enclosing run". But only test2json (-test.v=test2json) removes the escapes; in plain -test.v=true mode nothing does. A few lines further down in the same function the ^O/^N error markers are correctly gated on o.c.chatty.json; the escaping is not. The code is unchanged on master and release-branch.go1.27 as of 2026-09-17.

This is a regression from Go 1.26.

What did you expect to see?

The same output as Go 1.26: go test -v prints the logged bytes unchanged apart from the indentation and the file:line: prefix, and the framing escapes are applied only in -test.v=test2json mode, where test2json decodes them. For example, in writeLine:

	// Escape the framing marker.
	if o.c.chatty.json {
		b = escapeMarkers(b)
	}