From 6262aa56b18ea67470c474b4e61dd0314a4b93cb Mon Sep 17 00:00:00 2001 From: Andrey Kolkov Date: Fri, 11 Sep 2026 00:11:25 +0300 Subject: [PATCH 1/3] docs: update Go version requirement to 1.27+ in README --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 122c19d..86d82bd 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# ๐ŸŒŠ stream - Real-time Communications for Go 1.25+ +# ๐ŸŒŠ stream - Real-time Communications for Go 1.27+ > Server-Sent Events and WebSocket implementations - Zero external dependencies, RFC-compliant, production-ready @@ -135,7 +135,7 @@ Built-in Hub pattern for efficient message broadcasting to multiple clients with go get github.com/coregx/stream ``` -**Requirements**: Go 1.25+ (uses `encoding/json/v2` and modern generics) +**Requirements**: Go 1.27+ (uses `encoding/json/v2` and modern generics) --- @@ -164,7 +164,7 @@ go get github.com/coregx/stream ### Common Features - ๐Ÿš€ **Zero Dependencies** - Pure stdlib implementation -- ๐ŸŽฏ **Type-Safe** - Modern Go 1.25+ with generics +- ๐ŸŽฏ **Type-Safe** - Modern Go 1.27+ with generics - โšก **High Performance** - <100 ฮผs broadcasts, minimal allocations - ๐Ÿงช **Well-Tested** - 314 tests total, 84.3% coverage - ๐Ÿข **Production Ready** - Used in coregx ecosystem From 849a6d28e0a6946e64c5a8046b12d1a43bb1c730 Mon Sep 17 00:00:00 2001 From: Andrey Kolkov Date: Fri, 11 Sep 2026 00:30:35 +0300 Subject: [PATCH 2/3] fix: resolve all 16 golangci-lint issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - staticcheck: use fmt.Fprintf instead of WriteString(fmt.Sprintf) in SSE event - gosec: safe intโ†’uint16 via bounds clamp for WebSocket close code - nolintlint: remove 2 unused //nolint:gosec directives in frame.go - gocognit: annotate Hub.Run() select loop (inherent complexity) - prealloc: use slices.Concat for test frame construction (11 instances) --- sse/event.go | 2 +- websocket/conn.go | 5 ++-- websocket/frame.go | 2 -- websocket/frame_test.go | 60 +++++++++++++++-------------------------- websocket/hub.go | 1 + 5 files changed, 27 insertions(+), 43 deletions(-) diff --git a/sse/event.go b/sse/event.go index 959b13f..02d904a 100644 --- a/sse/event.go +++ b/sse/event.go @@ -130,7 +130,7 @@ func (e *Event) String() string { // Retry (optional) if e.Retry > 0 { b.WriteString("retry: ") - b.WriteString(fmt.Sprintf("%d", e.Retry)) + fmt.Fprintf(&b, "%d", e.Retry) b.WriteByte('\n') } diff --git a/websocket/conn.go b/websocket/conn.go index 3295dab..cbdbe65 100644 --- a/websocket/conn.go +++ b/websocket/conn.go @@ -3,6 +3,7 @@ package websocket import ( "bufio" "bytes" + "encoding/binary" "encoding/json/v2" "net" "sync" @@ -462,8 +463,8 @@ func (c *Conn) CloseWithCode(code CloseCode, reason string) error { // Build close frame payload: 2 bytes status code + optional reason payload := make([]byte, 2+len(reason)) - payload[0] = byte(code >> 8) - payload[1] = byte(code & 0xFF) + c16 := min(max(int(code), 0), 0xFFFF) + binary.BigEndian.PutUint16(payload, uint16(c16)) copy(payload[2:], reason) // Validate reason is valid UTF-8 diff --git a/websocket/frame.go b/websocket/frame.go index 42cbf2e..8df72e5 100644 --- a/websocket/frame.go +++ b/websocket/frame.go @@ -259,7 +259,6 @@ func writeFrame(w *bufio.Writer, f *frame) error { payloadLen := uint64(len(f.payload)) // Determine payload length encoding. - //nolint:gosec // G602: False positive - header is always length 2 switch { case payloadLen <= payloadLen7Bit: // 7-bit length (0-125). @@ -363,7 +362,6 @@ func writeFrameNoValidation(w *bufio.Writer, f *frame) error { payloadLen := uint64(len(f.payload)) // Determine payload length encoding. - //nolint:gosec // G602: False positive - header is always length 2 switch { case payloadLen <= payloadLen7Bit: // 7-bit length (0-125). diff --git a/websocket/frame_test.go b/websocket/frame_test.go index 692e700..e2bd7d3 100644 --- a/websocket/frame_test.go +++ b/websocket/frame_test.go @@ -6,6 +6,7 @@ import ( "encoding/binary" "errors" "io" + "slices" "strings" "testing" "unicode/utf8" @@ -54,12 +55,11 @@ func TestReadFrame_TextMasked(t *testing.T) { applyMask(masked, mask) // Frame: FIN=1, opcode=text(0x1), masked - data := []byte{ + data := slices.Concat([]byte{ 0x81, // FIN=1, RSV=0, opcode=0x1 (text) 0x85, // MASK=1, length=5 mask[0], mask[1], mask[2], mask[3], // Masking key - } - data = append(data, masked...) + }, masked) r := bufio.NewReader(bytes.NewReader(data)) f, err := readFrame(r, 0) @@ -84,11 +84,10 @@ func TestReadFrame_TextMasked(t *testing.T) { func TestReadFrame_Binary(t *testing.T) { payload := []byte{0x00, 0xFF, 0xAA, 0x55} - data := []byte{ + data := slices.Concat([]byte{ 0x82, // FIN=1, RSV=0, opcode=0x2 (binary) 0x04, // MASK=0, length=4 - } - data = append(data, payload...) + }, payload) r := bufio.NewReader(bytes.NewReader(data)) f, err := readFrame(r, 0) @@ -223,16 +222,13 @@ func TestReadFrame_ExtendedLength16(t *testing.T) { payloadLen := 1000 payload := bytes.Repeat([]byte("A"), payloadLen) - data := []byte{ - 0x81, // FIN=1, opcode=0x1 (text) - 126, // MASK=0, length=126 (triggers 16-bit) - } - // Write 16-bit length. lenBuf := make([]byte, 2) binary.BigEndian.PutUint16(lenBuf, uint16(payloadLen)) - data = append(data, lenBuf...) - data = append(data, payload...) + data := slices.Concat([]byte{ + 0x81, // FIN=1, opcode=0x1 (text) + 126, // MASK=0, length=126 (triggers 16-bit) + }, lenBuf, payload) r := bufio.NewReader(bytes.NewReader(data)) f, err := readFrame(r, 0) @@ -252,16 +248,13 @@ func TestReadFrame_ExtendedLength64(t *testing.T) { payloadLen := 70000 payload := bytes.Repeat([]byte("B"), payloadLen) - data := []byte{ - 0x82, // FIN=1, opcode=0x2 (binary) - 127, // MASK=0, length=127 (triggers 64-bit) - } - // Write 64-bit length. lenBuf := make([]byte, 8) binary.BigEndian.PutUint64(lenBuf, uint64(payloadLen)) - data = append(data, lenBuf...) - data = append(data, payload...) + data := slices.Concat([]byte{ + 0x82, // FIN=1, opcode=0x2 (binary) + 127, // MASK=0, length=127 (triggers 64-bit) + }, lenBuf, payload) r := bufio.NewReader(bytes.NewReader(data)) f, err := readFrame(r, 0) @@ -344,12 +337,11 @@ func TestReadFrame_ControlFragmented(t *testing.T) { // RFC 6455 Section 5.5: Control frames must have payload <= 125 bytes. func TestReadFrame_ControlTooLarge(t *testing.T) { // Control frame with 126-byte payload (invalid). - data := []byte{ + data := slices.Concat([]byte{ 0x88, // FIN=1, opcode=0x8 (close) 126, // MASK=0, length=126 (triggers 16-bit) 0x00, 0x7E, // 126 bytes - EXCEEDS 125 LIMIT! - } - data = append(data, make([]byte, 126)...) + }, make([]byte, 126)) r := bufio.NewReader(bytes.NewReader(data)) _, err := readFrame(r, 0) @@ -365,11 +357,10 @@ func TestReadFrame_InvalidUTF8(t *testing.T) { // Invalid UTF-8 sequence. invalidUTF8 := []byte{0xFF, 0xFE, 0xFD} - data := []byte{ + data := slices.Concat([]byte{ 0x81, // FIN=1, opcode=0x1 (text) 0x03, // MASK=0, length=3 - } - data = append(data, invalidUTF8...) + }, invalidUTF8) r := bufio.NewReader(bytes.NewReader(data)) _, err := readFrame(r, 0) @@ -427,8 +418,7 @@ func TestWriteFrame_Binary(t *testing.T) { } data := buf.Bytes() - expected := []byte{0x82, 0x04} - expected = append(expected, payload...) + expected := slices.Concat([]byte{0x82, 0x04}, payload) if !bytes.Equal(data, expected) { t.Errorf("expected %v, got %v", expected, data) @@ -892,8 +882,7 @@ func TestIsValidOpcode(t *testing.T) { // BenchmarkReadFrame_Small benchmarks reading small frames (< 126 bytes). func BenchmarkReadFrame_Small(b *testing.B) { payload := bytes.Repeat([]byte("A"), 100) - data := []byte{0x81, 0x64} // FIN=1, opcode=text, length=100 - data = append(data, payload...) + data := slices.Concat([]byte{0x81, 0x64}, payload) // FIN=1, opcode=text, length=100 b.ResetTimer() b.ReportAllocs() @@ -912,11 +901,9 @@ func BenchmarkReadFrame_Medium(b *testing.B) { payloadLen := 1000 payload := bytes.Repeat([]byte("B"), payloadLen) - data := []byte{0x81, 126} // 16-bit length lenBuf := make([]byte, 2) binary.BigEndian.PutUint16(lenBuf, uint16(payloadLen)) - data = append(data, lenBuf...) - data = append(data, payload...) + data := slices.Concat([]byte{0x81, 126}, lenBuf, payload) // 16-bit length b.ResetTimer() b.ReportAllocs() @@ -935,11 +922,9 @@ func BenchmarkReadFrame_Large(b *testing.B) { payloadLen := 100000 payload := bytes.Repeat([]byte("C"), payloadLen) - data := []byte{0x82, 127} // 64-bit length, binary lenBuf := make([]byte, 8) binary.BigEndian.PutUint64(lenBuf, uint64(payloadLen)) - data = append(data, lenBuf...) - data = append(data, payload...) + data := slices.Concat([]byte{0x82, 127}, lenBuf, payload) // 64-bit length, binary b.ResetTimer() b.ReportAllocs() @@ -1109,10 +1094,9 @@ func TestUTF8Validation(t *testing.T) { func TestMaxPayloadLength(t *testing.T) { // Test data frame at limit. payloadLen := defaultMaxFramePayload - data := []byte{0x82, 127} // Binary, 64-bit length lenBuf := make([]byte, 8) binary.BigEndian.PutUint64(lenBuf, uint64(payloadLen)) - data = append(data, lenBuf...) + data := slices.Concat([]byte{0x82, 127}, lenBuf) // Binary, 64-bit length // Don't actually create huge payload, just test header. r := bufio.NewReader(bytes.NewReader(data)) diff --git a/websocket/hub.go b/websocket/hub.go index 2706efa..e7c7337 100644 --- a/websocket/hub.go +++ b/websocket/hub.go @@ -88,6 +88,7 @@ func NewHub() *Hub { // Run exits when Close() is called. // Run must be called in a goroutine: go hub.Run(). // Call hub.AddRunning() before starting the goroutine if using wg externally. +//nolint:gocognit // Select loop with channel-close guards is inherently complex. func (h *Hub) Run() { h.mu.Lock() if h.started || h.closed { From c74eae6f937fb3e388c34c6c0d7bd7cb77698fea Mon Sep 17 00:00:00 2001 From: Andrey Kolkov Date: Fri, 11 Sep 2026 00:33:06 +0300 Subject: [PATCH 3/3] style: gofmt websocket/hub.go --- websocket/hub.go | 1 + 1 file changed, 1 insertion(+) diff --git a/websocket/hub.go b/websocket/hub.go index e7c7337..a14422d 100644 --- a/websocket/hub.go +++ b/websocket/hub.go @@ -88,6 +88,7 @@ func NewHub() *Hub { // Run exits when Close() is called. // Run must be called in a goroutine: go hub.Run(). // Call hub.AddRunning() before starting the goroutine if using wg externally. +// //nolint:gocognit // Select loop with channel-close guards is inherently complex. func (h *Hub) Run() { h.mu.Lock()