slog: TextHandler quotes []byte

Make TextHandler's output nicer for []byte values: quote them,
like strings, even if they contain binary.

Change-Id: I757823ed7ce7aff3e25b889de925e6252324d9d9
Reviewed-on: https://go-review.googlesource.com/c/exp/+/453495
Run-TryBot: Jonathan Amsterdam <jba@google.com>
Reviewed-by: Alan Donovan <adonovan@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
diff --git a/slog/handler_test.go b/slog/handler_test.go
index 9141a41..be4a925 100644
--- a/slog/handler_test.go
+++ b/slog/handler_test.go
@@ -8,6 +8,7 @@
 
 import (
 	"bytes"
+	"encoding/json"
 	"io"
 	"strings"
 	"testing"
@@ -267,6 +268,20 @@
 			wantText: "msg=message v=3",
 			wantJSON: `{"msg":"message","v":3}`,
 		},
+		{
+			name:     "byte slice",
+			replace:  removeKeys(TimeKey, LevelKey),
+			attrs:    []Attr{Any("bs", []byte{1, 2, 3, 4})},
+			wantText: `msg=message bs="\x01\x02\x03\x04"`,
+			wantJSON: `{"msg":"message","bs":"AQIDBA=="}`,
+		},
+		{
+			name:     "json.RawMessage",
+			replace:  removeKeys(TimeKey, LevelKey),
+			attrs:    []Attr{Any("bs", json.RawMessage([]byte("1234")))},
+			wantText: `msg=message bs="1234"`,
+			wantJSON: `{"msg":"message","bs":1234}`,
+		},
 	} {
 		r := NewRecord(testTime, InfoLevel, "message", 1, nil)
 		r.AddAttrs(test.attrs...)
diff --git a/slog/text_handler.go b/slog/text_handler.go
index b4622e5..191da44 100644
--- a/slog/text_handler.go
+++ b/slog/text_handler.go
@@ -8,6 +8,8 @@
 	"encoding"
 	"fmt"
 	"io"
+	"reflect"
+	"strconv"
 	"unicode"
 	"unicode/utf8"
 )
@@ -103,6 +105,11 @@
 			s.appendString(string(data))
 			return nil
 		}
+		if bs, ok := byteSlice(v.any); ok {
+			// As of Go 1.19, this only allocates for strings longer than 32 bytes.
+			s.buf.WriteString(strconv.Quote(string(bs)))
+			return nil
+		}
 		s.appendString(fmt.Sprint(v.Any()))
 	default:
 		*s.buf = v.append(*s.buf)
@@ -110,6 +117,21 @@
 	return nil
 }
 
+// byteSlice returns its argument as a []byte if the argument's
+// underlying type is []byte, along with a second return value of true.
+// Otherwise it returns nil, false.
+func byteSlice(a any) ([]byte, bool) {
+	if bs, ok := a.([]byte); ok {
+		return bs, true
+	}
+	// Like Printf's %s, we allow both the slice type and the byte element type to be named.
+	t := reflect.TypeOf(a)
+	if t.Kind() == reflect.Slice && t.Elem().Kind() == reflect.Uint8 {
+		return reflect.ValueOf(a).Bytes(), true
+	}
+	return nil, false
+}
+
 func needsQuoting(s string) bool {
 	for i := 0; i < len(s); {
 		b := s[i]