From c23c01ad0a13c70578c87abc6a77955314a8c839 Mon Sep 17 00:00:00 2001 From: mr-pmillz <58376398+mr-pmillz@users.noreply.github.com> Date: Thu, 8 Oct 2026 21:14:08 -0400 Subject: [PATCH] Modernize for Go 1.27.2 and pin GitHub Actions Upgrade the module and CI to Go 1.27.2, apply modernizers to a fixpoint, and migrate lint tooling. Pin workflow actions to verified stable release commits. Validated build, vet, tests, race tests, lint, formatting, and actionlint. --- .github/workflows/go.yml | 35 ++++++------ .golangci.yml | 81 ++++++++++++++++------------ README.md | 10 +--- client_test.go | 3 +- cmd/complex/main.go | 8 ++- cmd/complex/metrics.go | 2 +- cmd/complex_client/main.go | 2 +- cmd/llm/main.go | 2 - event.go | 2 +- go.mod | 2 +- internal/parser/field_parser_test.go | 2 - internal/parser/parser_test.go | 2 - internal/parser/split_func_test.go | 2 - joe.go | 8 +-- message_fields.go | 2 +- message_fields_test.go | 2 - replay.go | 9 ++-- replay_test.go | 2 +- server_test.go | 2 +- 19 files changed, 84 insertions(+), 94 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 58311bd..5e2e6b1 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -16,35 +16,30 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: golangci/golangci-lint-action@v2 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version-file: go.mod + - uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9.3.0 + with: + version: v2.14.0 + - name: Check formatting + run: golangci-lint fmt --diff test: - name: Test (latest major) + name: Test runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-go@v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: - go-version: "~1.23.4" + go-version-file: go.mod - name: Test run: go test -v -timeout=1s -coverprofile=coverage.txt -covermode=atomic ./... - name: Test (race) run: go test -v -timeout=1s -race ./... - name: Coverage - uses: codecov/codecov-action@v3 - if: github.ref == 'refs/head/master' + uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1 + if: github.ref == 'refs/heads/master' with: token: ${{ secrets.CODECOV_TOKEN }} files: ./coverage.txt - test-old: - name: Test (previous major) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-go@v4 - with: - go-version: "~1.22.10" - - name: Test - run: go test -v -timeout=1s ./... - - name: Test (race) - run: go test -v -timeout=1s -race ./... diff --git a/.golangci.yml b/.golangci.yml index 2ce58bb..ad240cc 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,58 +1,71 @@ +version: "2" linters: enable: - - errcheck - - gosimple - - govet - - ineffassign - - staticcheck - - typecheck - dogsled - dupl - errorlint - exhaustive - - nestif - goconst - gocritic - gocyclo - godot - godox - - gofmt - - gofumpt - goheader - - goimports - gomoddirectives - - gomodguard + - gomodguard_v2 - gosec - importas - makezero - misspell + - nestif + - nolintlint - prealloc - - promlinter - predeclared - - nolintlint + - promlinter - revive - - stylecheck + - staticcheck - tagliatelle - thelper - unparam - - unused - whitespace -linters-settings: - gosec: - excludes: - - G404 - gocritic: - disabled-checks: - - ifElseChain - - unnamedResult - - hugeParam - enabled-tags: - - performance - - diagnostic - - experimental - - opinionated - nestif: - min-complexity: 8 - govet: - enable: - - fieldalignment + settings: + gocritic: + disabled-checks: + - ifElseChain + - unnamedResult + - hugeParam + enabled-tags: + - performance + - diagnostic + - experimental + - opinionated + gosec: + excludes: + - G404 + govet: + enable: + - fieldalignment + nestif: + min-complexity: 8 + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + paths: + - third_party$ + - builtin$ + - examples$ +formatters: + enable: + - gofmt + - gofumpt + - goimports + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/README.md b/README.md index 35f92e5..093b22a 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ The library provides both server-side and client-side implementations of the pro If you are not familiar with the protocol or not sure how it works, read [MDN's guide for using server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events). [The spec](https://html.spec.whatwg.org/multipage/server-sent-events.html) is also useful read! -`go-sse` promises to support the [Go versions supported by the Go team](https://go.dev/doc/devel/release#policy) – that is, the 2 most recent major releases. +`go-sse` requires Go 1.27.2 or later. ## Cut to the chase – how do I read my LLM's response? @@ -70,13 +70,7 @@ for ev, err := range sse.Read(res.Body, nil) { See the [LLM example](cmd/llm/main.go) for a fully working Go program. -Go 1.23 iterators (officially ["range-over-func"](https://go.dev/blog/range-functions)) are used for this feature. If you are still on Go 1.22 use the `GOEXPERIMENT=rangefunc` environment variable (e.g. `GOEXPERIMENT=rangefunc go run main.go`) or use the iterator without the syntactic sugar: -```go -events(func(ev Event) bool { - // do something with event - return true // or false to stop iteration -}) -``` +This feature uses Go's [range-over-function iterators](https://go.dev/blog/range-functions). `sse.Read` is also useful if you're implementing an LLM SDK: call it in your code and spare yourself time and maintenance burden by not reimplementing event stream parsing. diff --git a/client_test.go b/client_test.go index 235961c..2687260 100644 --- a/client_test.go +++ b/client_test.go @@ -565,8 +565,7 @@ func TestConnection_serverError(t *testing.T) { ResponseValidator: sse.NoopValidator, Backoff: sse.Backoff{MaxRetries: -1}, } - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + ctx := t.Context() conn := c.NewConnection(reqCtx(t, ctx, "", ts.URL, nil)) all, unsubAll := events(t, conn) diff --git a/cmd/complex/main.go b/cmd/complex/main.go index 46b908f..ae5de9e 100644 --- a/cmd/complex/main.go +++ b/cmd/complex/main.go @@ -37,11 +37,9 @@ func newSSE() *sse.Server { topics = r.URL.Query()["topic"] for _, topic := range topics { if topic != topicRandomNumbers && topic != topicMetrics { - fmt.Fprintf(w, "invalid topic %q; supported are %q, %q", topic, topicRandomNumbers, topicMetrics) - // NOTE: if you are returning false to reject the subscription, we strongly recommend writing // your own response code. Clients will receive a 200 code otherwise, which may be confusing. - w.WriteHeader(http.StatusBadRequest) + http.Error(w, fmt.Sprintf("invalid topic %q; supported are %q, %q", topic, topicRandomNumbers, topicMetrics), http.StatusBadRequest) return nil, false } } @@ -161,7 +159,7 @@ func runServer(ctx context.Context, s *http.Server) error { go func() { <-ctx.Done() - sctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + sctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), time.Second*10) defer cancel() shutdownError <- s.Shutdown(sctx) @@ -178,7 +176,7 @@ func generateRandomNumbers() *sse.Message { e := &sse.Message{} count := 1 + rand.Intn(5) - for i := 0; i < count; i++ { + for range count { e.AppendData(strconv.FormatUint(rand.Uint64(), 10)) } diff --git a/cmd/complex/metrics.go b/cmd/complex/metrics.go index b578d0c..b1a723c 100644 --- a/cmd/complex/metrics.go +++ b/cmd/complex/metrics.go @@ -27,7 +27,7 @@ func Inc(metric string) int64 { // Range loops through all metrics and calls the given function for each metric. func Range(fn func(key string, value int64) bool) { - metrics.Range(func(key, value interface{}) bool { + metrics.Range(func(key, value any) bool { return fn(key.(string), value.(int64)) }) } diff --git a/cmd/complex_client/main.go b/cmd/complex_client/main.go index f9411f8..7ca7344 100644 --- a/cmd/complex_client/main.go +++ b/cmd/complex_client/main.go @@ -35,7 +35,7 @@ func main() { cancel() default: // no event name var sum, num big.Int - for _, n := range strings.Split(event.Data, "\n") { + for n := range strings.SplitSeq(event.Data, "\n") { _, _ = num.SetString(n, 10) sum.Add(&sum, &num) } diff --git a/cmd/llm/main.go b/cmd/llm/main.go index 230e5e5..737d947 100644 --- a/cmd/llm/main.go +++ b/cmd/llm/main.go @@ -1,5 +1,3 @@ -//go:build go1.23 - package main import ( diff --git a/event.go b/event.go index 989bb9d..c23c28e 100644 --- a/event.go +++ b/event.go @@ -126,7 +126,7 @@ func read(pf func() *parser.Parser, lastEventID string, onRetry func(int64), ign } } - if err != nil && !(ignoreEOF && isEOF) { + if err != nil && (!ignoreEOF || !isEOF) { yield(Event{}, err) } } diff --git a/go.mod b/go.mod index 2813dd4..bbbc5de 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ module github.com/tmaxmax/go-sse -go 1.22 +go 1.27.2 diff --git a/internal/parser/field_parser_test.go b/internal/parser/field_parser_test.go index d9c441b..62715bb 100644 --- a/internal/parser/field_parser_test.go +++ b/internal/parser/field_parser_test.go @@ -91,8 +91,6 @@ func TestFieldParser(t *testing.T) { } for _, test := range tests { - test := test - t.Run(test.name, func(t *testing.T) { t.Parallel() diff --git a/internal/parser/parser_test.go b/internal/parser/parser_test.go index 7af7492..98b29f3 100644 --- a/internal/parser/parser_test.go +++ b/internal/parser/parser_test.go @@ -105,8 +105,6 @@ data: still, here's some data: you deserve it } for _, test := range tests { - test := test - t.Run(test.name, func(t *testing.T) { t.Parallel() diff --git a/internal/parser/split_func_test.go b/internal/parser/split_func_test.go index ede11ed..0bec982 100644 --- a/internal/parser/split_func_test.go +++ b/internal/parser/split_func_test.go @@ -40,8 +40,6 @@ func TestSplitFunc(t *testing.T) { } for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { t.Parallel() diff --git a/joe.go b/joe.go index d693774..dae6722 100644 --- a/joe.go +++ b/joe.go @@ -171,9 +171,11 @@ func (j *Joe) Publish(msg *Message, topics []string) error { // message published causes an error after the shutdown. errs := make(chan error, 1) - pub := publishedMessage{replayerErr: errs} - pub.message = msg - pub.topics = topics + pub := publishedMessage{ + replayerErr: errs, + message: msg, + topics: topics, + } // Waiting on done ensures Publish doesn't block the caller goroutine // when Joe is stopped and implements the required Provider behavior. diff --git a/message_fields.go b/message_fields.go index 4fb6042..e3057fc 100644 --- a/message_fields.go +++ b/message_fields.go @@ -151,7 +151,7 @@ func (i *messageField) MarshalJSON() ([]byte, error) { // - nil interfaces (result: unset value) // - byte slice // - string -func (i *messageField) Scan(src interface{}) error { +func (i *messageField) Scan(src any) error { *i = messageField{} if src == nil { diff --git a/message_fields_test.go b/message_fields_test.go index db92e72..1d15460 100644 --- a/message_fields_test.go +++ b/message_fields_test.go @@ -48,8 +48,6 @@ func TestMessageField_UnmarshalJSON(t *testing.T) { } for _, test := range tt { - test := test - t.Run(test.name, func(t *testing.T) { t.Parallel() diff --git a/replay.go b/replay.go index 6cbe015..ddf0981 100644 --- a/replay.go +++ b/replay.go @@ -2,6 +2,7 @@ package sse import ( "errors" + "slices" "strconv" "time" ) @@ -157,7 +158,7 @@ func (v *ValidReplayer) Put(message *Message, topics []string) (*Message, error) v.messages.resize(newCap) } - v.messages.enqueue(messageWithTopicsAndExpiry{messageWithTopics: messageWithTopics{message: message, topics: topics}, exp: now.Add(v.ttl)}) + v.messages.enqueue(messageWithTopicsAndExpiry{message: message, topics: topics, exp: now.Add(v.ttl)}) return message, nil } @@ -218,10 +219,8 @@ func (v *ValidReplayer) Replay(subscription Subscription) error { // topicsIntersect returns true if the given topic slices have at least one topic in common. func topicsIntersect(a, b []string) bool { for _, at := range a { - for _, bt := range b { - if at == bt { - return true - } + if slices.Contains(b, at) { + return true } } diff --git a/replay_test.go b/replay_test.go index 11b85ca..bb47ddc 100644 --- a/replay_test.go +++ b/replay_test.go @@ -186,7 +186,7 @@ func TestFiniteReplayProvider_allocations(t *testing.T) { queue := make([]*sse.Message, runs+1) lastID := runs - for i := 0; i < len(queue); i++ { + for i := range queue { queue[i] = msg(t, fmt.Sprintf("message %d", i), strconv.Itoa(i), diff --git a/server_test.go b/server_test.go index 8093674..27f58c8 100644 --- a/server_test.go +++ b/server_test.go @@ -263,7 +263,7 @@ func benchmarkServer(b *testing.B, conns int) { m := getMessage(b) - for i := 0; i < conns; i++ { + for range conns { w, r := getRequest(b) go s.ServeHTTP(w, r) }