slog: have Logger.With,WithGroup preserve context

When we create a new Logger using With or WithGroup, preserve
the context from the original Logger.

Change-Id: I62cc73aaff50d6ae755ca57f5a934604c4645f2e
Reviewed-on: https://go-review.googlesource.com/c/exp/+/459615
TryBot-Result: Gopher Robot <gobot@golang.org>
Run-TryBot: Jonathan Amsterdam <jba@google.com>
Reviewed-by: Alan Donovan <adonovan@google.com>
diff --git a/slog/logger.go b/slog/logger.go
index 08dc7ce..d949a3d 100644
--- a/slog/logger.go
+++ b/slog/logger.go
@@ -69,15 +69,20 @@
 	ctx     context.Context
 }
 
+func (l *Logger) clone() *Logger {
+	c := *l
+	return &c
+}
+
 // Handler returns l's Handler.
 func (l *Logger) Handler() Handler { return l.handler }
 
-// Context returns l's context.
+// Context returns l's context, which may be nil.
 func (l *Logger) Context() context.Context { return l.ctx }
 
 // With returns a new Logger that includes the given arguments, converted to
 // Attrs as in [Logger.Log]. The Attrs will be added to each output from the
-// Logger.
+// Logger. The new Logger shares the old Logger's context.
 //
 // The new Logger's handler is the result of calling WithAttrs on the receiver's
 // handler.
@@ -90,28 +95,40 @@
 		attr, args = argsToAttr(args)
 		attrs = append(attrs, attr)
 	}
-	return New(l.handler.WithAttrs(attrs))
+	c := l.clone()
+	c.handler = l.handler.WithAttrs(attrs)
+	return c
 }
 
 // WithGroup returns a new Logger that starts a group. The keys of all
 // attributes added to the Logger will be qualified by the given name.
+// The new Logger shares the old Logger's context.
 //
 // The new Logger's handler is the result of calling WithGroup on the receiver's
 // handler.
 func (l *Logger) WithGroup(name string) *Logger {
-	return New(l.handler.WithGroup(name))
+	c := l.clone()
+	c.handler = l.handler.WithGroup(name)
+	return c
+
 }
 
 // WithContext returns a new Logger with the same handler
 // as the receiver and the given context.
+// It uses the same handler as the original.
 func (l *Logger) WithContext(ctx context.Context) *Logger {
-	l2 := *l
-	l2.ctx = ctx
-	return &l2
+	c := l.clone()
+	c.ctx = ctx
+	return c
 }
 
-// New creates a new Logger with the given Handler.
-func New(h Handler) *Logger { return &Logger{handler: h} }
+// New creates a new Logger with the given non-nil Handler and a nil context.
+func New(h Handler) *Logger {
+	if h == nil {
+		panic("nil Handler")
+	}
+	return &Logger{handler: h}
+}
 
 // With calls Logger.With on the default logger.
 func With(args ...any) *Logger {
diff --git a/slog/logger_test.go b/slog/logger_test.go
index 141307a..5f40cc1 100644
--- a/slog/logger_test.go
+++ b/slog/logger_test.go
@@ -15,6 +15,8 @@
 	"strings"
 	"testing"
 	"time"
+
+	"golang.org/x/exp/slices"
 )
 
 const timeRE = `\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}(Z|[+-]\d{2}:\d{2})`
@@ -347,6 +349,46 @@
 	checkLogOutput(t, buf.String(), `level=ERROR msg=msg !BADKEY=a err=EOF`)
 }
 
+func TestLogCopying(t *testing.T) {
+	// Verify that Logger methods that purport to set one field of a new Logger
+	// actually do so while preserving the other field.
+
+	h := &captureHandler{} // Use a captureHandler for convenience.
+	l := New(h)
+	ctx := context.WithValue(context.Background(), "v", 0)
+
+	checkContext := func(l *Logger) {
+		t.Helper()
+		ctx := l.Context()
+		if ctx == nil {
+			t.Error("nil context")
+		} else if got, want := ctx.Value("v"), 0; got != want {
+			t.Errorf("for got %v, want %v", got, want)
+		}
+	}
+
+	// WithContext returns a Logger with the given context and the same handler.
+	l2 := l.WithContext(ctx)
+	checkContext(l2)
+	if l2.Handler() != h {
+		t.Error("WithContext changed handler")
+	}
+
+	// With returns a Logger with a different handler but the same context.
+	l3 := l2.With("a", 1)
+	if l3.Handler() == l2.Handler() {
+		t.Error("With did not change handler")
+	}
+	checkContext(l3)
+
+	// WithGroup also returns a Logger with a different handler but the same context.
+	l4 := l3.WithGroup("g")
+	if l4.Handler() == l3.Handler() {
+		t.Error("With did not change handler")
+	}
+	checkContext(l4)
+}
+
 func checkLogOutput(t *testing.T, got, wantRegexp string) {
 	t.Helper()
 	got = clean(got)
@@ -369,8 +411,9 @@
 }
 
 type captureHandler struct {
-	r     Record
-	attrs []Attr
+	r      Record
+	attrs  []Attr
+	groups []string
 }
 
 func (h *captureHandler) Handle(r Record) error {
@@ -386,8 +429,10 @@
 	return &c2
 }
 
-func (h *captureHandler) WithGroup(name string) Handler {
-	panic("unimplemented")
+func (c *captureHandler) WithGroup(name string) Handler {
+	c2 := *c
+	c2.groups = append(slices.Clip(c2.groups), name)
+	return &c2
 }
 
 type discardHandler struct {