Repository: quic-go/quic-go
Pull request #5007 (2025-03-25 14:36:16)

ci: update golangci-lint to v2

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/quic-go/quic-go/pull/5007?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=quic-go) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 84.15%. Comparing base [(`623e59b`)](https://app.codecov.io/gh/quic-go/quic-go/commit/623e59b2a22e862ef2a5702acb0de2bf96373d14?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=quic-go) to head [(`c4e4cef`)](https://app.codecov.io/gh/quic-go/quic-go/commit/c4e4cef7e71f1f98cc04ace1baa0db279f4080b1?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=quic-go).

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #5007      +/-   ##
==========================================
+ Coverage   84.12%   84.15%   +0.02%     
==========================================
  Files         153      153              
  Lines       16904    16904              
==========================================
+ Hits        14220    14224       +4     
+ Misses       2153     2150       -3     
+ Partials      531      530       -1     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/quic-go/quic-go/pull/5007?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=quic-go).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=quic-go).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>

Patch:
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 6006b36a..b8b00f0b 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -41,7 +41,7 @@ jobs:
       matrix:
         go: [  "1.23.x", "1.24.x" ]
     env:
-      GOLANGCI_LINT_VERSION: v1.64.4
+      GOLANGCI_LINT_VERSION: v2.0.2
     name: golangci-lint (Go ${{ matrix.go }})
     steps:
       - uses: actions/checkout@v4
@@ -49,13 +49,13 @@ jobs:
         with:
           go-version: ${{ matrix.go }}
       - name: golangci-lint (Linux)
-        uses: golangci/golangci-lint-action@v6
+        uses: golangci/golangci-lint-action@v7
         with:
           args: --timeout=3m
           version: ${{ env.GOLANGCI_LINT_VERSION }}
       - name: golangci-lint (Windows)
         if: success() || failure() # run this step even if the previous one failed
-        uses: golangci/golangci-lint-action@v6
+        uses: golangci/golangci-lint-action@v7
         env:
           GOOS: "windows"
         with:
@@ -63,7 +63,7 @@ jobs:
           version: ${{ env.GOLANGCI_LINT_VERSION }}
       - name: golangci-lint (OSX)
         if: success() || failure() # run this step even if the previous one failed
-        uses: golangci/golangci-lint-action@v6
+        uses: golangci/golangci-lint-action@v7
         env:
           GOOS: "darwin"
         with:
@@ -71,7 +71,7 @@ jobs:
           version: ${{ env.GOLANGCI_LINT_VERSION }}
       - name: golangci-lint (FreeBSD)
         if: success() || failure() # run this step even if the previous one failed
-        uses: golangci/golangci-lint-action@v6
+        uses: golangci/golangci-lint-action@v7
         env:
           GOOS: "freebsd"
         with:
@@ -79,7 +79,7 @@ jobs:
           version: ${{ env.GOLANGCI_LINT_VERSION }}
       - name: golangci-lint (others)
         if: success() || failure() # run this step even if the previous one failed
-        uses: golangci/golangci-lint-action@v6
+        uses: golangci/golangci-lint-action@v7
         env:
           GOOS: "solaris" # some OS that we don't have any build tags for
         with:
diff --git a/.golangci.yml b/.golangci.yml
index 1174b125..cf30c9d4 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -1,51 +1,66 @@
-linters-settings:
-  misspell:
-    ignore-words:
-      - ect
-  depguard:
-    rules:
-      quicvarint:
-        list-mode: strict
-        files:
-          - "**/github.com/quic-go/quic-go/quicvarint/*"
-          - "!$test"
-        allow:
-          - $gostd
-
+version: "2"
 linters:
-  disable-all: true
+  default: none
   enable:
     - asciicheck
     - copyloopvar
     - depguard
     - exhaustive
-    - goimports
-    - gofmt # redundant, since gofmt *should* be a no-op after gofumpt
-    - gofumpt
-    - gosimple
     - govet
     - ineffassign
     - misspell
     - prealloc
     - staticcheck
-    - stylecheck
     - unconvert
     - unparam
     - unused
-
-issues:
-  exclude-files:
-    - internal/handshake/cipher_suite.go
-  exclude-rules:
-    - path: internal/qtls
-      linters:
-        - depguard
-    - path: _test\.go
-      linters:
-        - exhaustive
-        - prealloc
-        - unparam
-    - path: _test\.go
-      text: "SA1029:"
-      linters:
-        - staticcheck
+  settings:
+    depguard:
+      rules:
+        quicvarint:
+          list-mode: strict
+          files:
+            - '**/github.com/quic-go/quic-go/quicvarint/*'
+            - '!$test'
+          allow:
+            - $gostd
+    misspell:
+      ignore-rules:
+        - ect
+  exclusions:
+    generated: lax
+    presets:
+      - comments
+      - common-false-positives
+      - legacy
+      - std-error-handling
+    rules:
+      - linters:
+          - depguard
+        path: internal/qtls
+      - linters:
+          - exhaustive
+          - prealloc
+          - unparam
+        path: _test\.go
+      - linters:
+          - staticcheck
+        path: _test\.go
+        text: 'SA1029:'
+    paths:
+      - internal/handshake/cipher_suite.go
+      - third_party$
+      - builtin$
+      - examples$
+formatters:
+  enable:
+    - gofmt
+    - gofumpt
+    - goimports
+  exclusions:
+    generated: lax
+    paths:
+      - internal/handshake/cipher_suite.go
+      - third_party$
+      - builtin$
+      - examples$
diff --git a/connection.go b/connection.go
index ef8476eb..4ed639ac 100644
--- a/connection.go
+++ b/connection.go
@@ -597,7 +597,7 @@ runLoop:
 		// We don't need to wait for new events if:
 		// * we processed packets: we probably need to send an ACK, and potentially more data
 		// * the pacer allows us to send more packets immediately
-		shouldProceedImmediately := sendQueueAvailable == nil && (processed || s.pacingDeadline == deadlineSendImmediately)
+		shouldProceedImmediately := sendQueueAvailable == nil && (processed || s.pacingDeadline.Equal(deadlineSendImmediately))
 		if !shouldProceedImmediately {
 			// 3rd: wait for something to happen:
 			// * closing of the connection
@@ -2141,10 +2141,11 @@ func (s *connection) sendPackets(now time.Time) error {
 		if err := s.sendPackedCoalescedPacket(packet, s.sentPacketHandler.ECNMode(packet.IsOnlyShortHeaderPacket()), now); err != nil {
 			return err
 		}
-		sendMode := s.sentPacketHandler.SendMode(now)
-		if sendMode == ackhandler.SendPacingLimited {
+		//nolint:exhaustive // only need to handle pacing-related events here
+		switch s.sentPacketHandler.SendMode(now) {
+		case ackhandler.SendPacingLimited:
 			s.resetPacingDeadline()
-		} else if sendMode == ackhandler.SendAny {
+		case ackhandler.SendAny:
 			s.pacingDeadline = deadlineSendImmediately
 		}
 		return nil
diff --git a/http3/client.go b/http3/client.go
index 9214bb80..91f43e41 100644
--- a/http3/client.go
+++ b/http3/client.go
@@ -109,24 +109,24 @@ func newClientConn(
 			if c.logger != nil {
 				c.logger.Debug("Setting up connection failed", "error", err)
 			}
-			c.connection.CloseWithError(quic.ApplicationErrorCode(ErrCodeInternalError), "")
+			c.CloseWithError(quic.ApplicationErrorCode(ErrCodeInternalError), "")
 		}
 	}()
 	if streamHijacker != nil {
 		go c.handleBidirectionalStreams(streamHijacker)
 	}
-	go c.connection.handleUnidirectionalStreams(uniStreamHijacker)
+	go c.handleUnidirectionalStreams(uniStreamHijacker)
 	return c
 }
 
 // OpenRequestStream opens a new request stream on the HTTP/3 connection.
 func (c *ClientConn) OpenRequestStream(ctx context.Context) (RequestStream, error) {
-	return c.connection.openRequestStream(ctx, c.requestWriter, nil, c.disableCompression, c.maxResponseHeaderBytes)
+	return c.openRequestStream(ctx, c.requestWriter, nil, c.disableCompression, c.maxResponseHeaderBytes)
 }
 
 func (c *ClientConn) setupConn() error {
 	// open the control stream
-	str, err := c.connection.OpenUniStream()
+	str, err := c.OpenUniStream()
 	if err != nil {
 		return err
 	}
@@ -140,7 +140,7 @@ func (c *ClientConn) setupConn() error {
 
 func (c *ClientConn) handleBidirectionalStreams(streamHijacker func(FrameType, quic.ConnectionTracingID, quic.Stream, error) (hijacked bool, err error)) {
 	for {
-		str, err := c.connection.AcceptStream(context.Background())
+		str, err := c.AcceptStream(context.Background())
 		if err != nil {
 			if c.logger != nil {
 				c.logger.Debug("accepting bidirectional stream failed", "error", err)
@@ -151,7 +151,7 @@ func (c *ClientConn) handleBidirectionalStreams(streamHijacker func(FrameType, q
 			r:    str,
 			conn: &c.connection,
 			unknownFrameHandler: func(ft FrameType, e error) (processed bool, err error) {
-				id := c.connection.Context().Value(quic.ConnectionTracingKey).(quic.ConnectionTracingID)
+				id := c.Context().Value(quic.ConnectionTracingKey).(quic.ConnectionTracingID)
 				return streamHijacker(ft, id, str, e)
 			},
 		}
@@ -164,7 +164,7 @@ func (c *ClientConn) handleBidirectionalStreams(streamHijacker func(FrameType, q
 					c.logger.Debug("error handling stream", "error", err)
 				}
 			}
-			c.connection.CloseWithError(quic.ApplicationErrorCode(ErrCodeFrameUnexpected), "received HTTP/3 frame on bidirectional stream")
+			c.CloseWithError(quic.ApplicationErrorCode(ErrCodeFrameUnexpected), "received HTTP/3 frame on bidirectional stream")
 		}()
 	}
 }
@@ -210,17 +210,17 @@ func (c *ClientConn) roundTrip(req *http.Request) (*http.Response, error) {
 		connCtx := c.Connection.Context()
 		// wait for the server's SETTINGS frame to arrive
 		select {
-		case <-c.connection.ReceivedSettings():
+		case <-c.ReceivedSettings():
 		case <-connCtx.Done():
 			return nil, context.Cause(connCtx)
 		}
-		if !c.connection.Settings().EnableExtendedConnect {
+		if !c.Settings().EnableExtendedConnect {
 			return nil, errors.New("http3: server didn't enable Extended CONNECT")
 		}
 	}
 
 	reqDone := make(chan struct{})
-	str, err := c.connection.openRequestStream(
+	str, err := c.openRequestStream(
 		req.Context(),
 		c.requestWriter,
 		reqDone,
@@ -350,7 +350,7 @@ func (c *ClientConn) doRequest(req *http.Request, str *requestStream) (*http.Res
 		}
 		break
 	}
-	connState := c.connection.ConnectionState().TLS
+	connState := c.ConnectionState().TLS
 	res.TLS = &connState
 	res.Request = req
 	return res, nil
diff --git a/http3/conn.go b/http3/conn.go
index bb17a5e5..1a9a410e 100644
--- a/http3/conn.go
+++ b/http3/conn.go
@@ -106,7 +106,7 @@ func (c *connection) openRequestStream(
 	disableCompression bool,
 	maxHeaderBytes uint64,
 ) (*requestStream, error) {
-	str, err := c.Connection.OpenStreamSync(ctx)
+	str, err := c.OpenStreamSync(ctx)
 	if err != nil {
 		return nil, err
 	}
@@ -180,7 +180,7 @@ func (c *connection) handleUnidirectionalStreams(hijack func(StreamType, quic.Co
 	)
 
 	for {
-		str, err := c.Connection.AcceptUniStream(context.Background())
+		str, err := c.AcceptUniStream(context.Background())
 		if err != nil {
 			if c.logger != nil {
 				c.logger.Debug("accepting unidirectional stream failed", "error", err)
@@ -191,7 +191,7 @@ func (c *connection) handleUnidirectionalStreams(hijack func(StreamType, quic.Co
 		go func(str quic.ReceiveStream) {
 			streamType, err := quicvarint.Read(quicvarint.NewReader(str))
 			if err != nil {
-				id := c.Connection.Context().Value(quic.ConnectionTracingKey).(quic.ConnectionTracingID)
+				id := c.Context().Value(quic.ConnectionTracingKey).(quic.ConnectionTracingID)
 				if hijack != nil && hijack(StreamType(streamType), id, str, err) {
 					return
 				}
@@ -205,13 +205,13 @@ func (c *connection) handleUnidirectionalStreams(hijack func(StreamType, quic.Co
 			case streamTypeControlStream:
 			case streamTypeQPACKEncoderStream:
 				if isFirst := rcvdQPACKEncoderStr.CompareAndSwap(false, true); !isFirst {
-					c.Connection.CloseWithError(quic.ApplicationErrorCode(ErrCodeStreamCreationError), "duplicate QPACK encoder stream")
+					c.CloseWithError(quic.ApplicationErrorCode(ErrCodeStreamCreationError), "duplicate QPACK encoder stream")
 				}
 				// Our QPACK implementation doesn't use the dynamic table yet.
 				return
 			case streamTypeQPACKDecoderStream:
 				if isFirst := rcvdQPACKDecoderStr.CompareAndSwap(false, true); !isFirst {
-					c.Connection.CloseWithError(quic.ApplicationErrorCode(ErrCodeStreamCreationError), "duplicate QPACK decoder stream")
+					c.CloseWithError(quic.ApplicationErrorCode(ErrCodeStreamCreationError), "duplicate QPACK decoder stream")
 				}
 				// Our QPACK implementation doesn't use the dynamic table yet.
 				return
@@ -219,17 +219,17 @@ func (c *connection) handleUnidirectionalStreams(hijack func(StreamType, quic.Co
 				switch c.perspective {
 				case protocol.PerspectiveClient:
 					// we never increased the Push ID, so we don't expect any push streams
-					c.Connection.CloseWithError(quic.ApplicationErrorCode(ErrCodeIDError), "")
+					c.CloseWithError(quic.ApplicationErrorCode(ErrCodeIDError), "")
 				case protocol.PerspectiveServer:
 					// only the server can push
-					c.Connection.CloseWithError(quic.ApplicationErrorCode(ErrCodeStreamCreationError), "")
+					c.CloseWithError(quic.ApplicationErrorCode(ErrCodeStreamCreationError), "")
 				}
 				return
 			default:
 				if hijack != nil {
 					if hijack(
 						StreamType(streamType),
-						c.Connection.Context().Value(quic.ConnectionTracingKey).(quic.ConnectionTracingID),
+						c.Context().Value(quic.ConnectionTracingKey).(quic.ConnectionTracingID),
 						str,
 						nil,
 					) {
@@ -267,8 +267,8 @@ func (c *connection) handleUnidirectionalStreams(hijack func(StreamType, quic.Co
 			// If datagram support was enabled on our side as well as on the server side,
 			// we can expect it to have been negotiated both on the transport and on the HTTP/3 layer.
 			// Note: ConnectionState() will block until the handshake is complete (relevant when using 0-RTT).
-			if c.enableDatagrams && !c.Connection.ConnectionState().SupportsDatagrams {
-				c.Connection.CloseWithError(quic.ApplicationErrorCode(ErrCodeSettingsError), "missing QUIC Datagram support")
+			if c.enableDatagrams && !c.ConnectionState().SupportsDatagrams {
+				c.CloseWithError(quic.ApplicationErrorCode(ErrCodeSettingsError), "missing QUIC Datagram support")
 				return
 			}
 			go func() {
@@ -287,22 +287,22 @@ func (c *connection) sendDatagram(streamID protocol.StreamID, b []byte) error {
 	data := make([]byte, 0, len(b)+8)
 	data = quicvarint.Append(data, uint64(streamID/4))
 	data = append(data, b...)
-	return c.Connection.SendDatagram(data)
+	return c.SendDatagram(data)
 }
 
 func (c *connection) receiveDatagrams() error {
 	for {
-		b, err := c.Connection.ReceiveDatagram(context.Background())
+		b, err := c.ReceiveDatagram(context.Background())
 		if err != nil {
 			return err
 		}
 		quarterStreamID, n, err := quicvarint.Parse(b)
 		if err != nil {
-			c.Connection.CloseWithError(quic.ApplicationErrorCode(ErrCodeDatagramError), "")
+			c.CloseWithError(quic.ApplicationErrorCode(ErrCodeDatagramError), "")
 			return fmt.Errorf("could not read quarter stream id: %w", err)
 		}
 		if quarterStreamID > maxQuarterStreamID {
-			c.Connection.CloseWithError(quic.ApplicationErrorCode(ErrCodeDatagramError), "")
+			c.CloseWithError(quic.ApplicationErrorCode(ErrCodeDatagramError), "")
 			return fmt.Errorf("invalid quarter stream id: %w", err)
 		}
 		streamID := protocol.StreamID(4 * quarterStreamID)
diff --git a/http3/http_stream.go b/http3/http_stream.go
index 4593d473..e030ab97 100644
--- a/http3/http_stream.go
+++ b/http3/http_stream.go
@@ -211,8 +211,8 @@ func (s *requestStream) ReadResponse() (*http.Response, error) {
 	}
 	frame, err := fp.ParseNext()
 	if err != nil {
-		s.Stream.CancelRead(quic.StreamErrorCode(ErrCodeFrameError))
-		s.Stream.CancelWrite(quic.StreamErrorCode(ErrCodeFrameError))
+		s.CancelRead(quic.StreamErrorCode(ErrCodeFrameError))
+		s.CancelWrite(quic.StreamErrorCode(ErrCodeFrameError))
 		return nil, fmt.Errorf("http3: parsing frame failed: %w", err)
 	}
 	hf, ok := frame.(*headersFrame)
@@ -221,14 +221,14 @@ func (s *requestStream) ReadResponse() (*http.Response, error) {
 		return nil, errors.New("http3: expected first frame to be a HEADERS frame")
 	}
 	if hf.Length > s.maxHeaderBytes {
-		s.Stream.CancelRead(quic.StreamErrorCode(ErrCodeFrameError))
-		s.Stream.CancelWrite(quic.StreamErrorCode(ErrCodeFrameError))
+		s.CancelRead(quic.StreamErrorCode(ErrCodeFrameError))
+		s.CancelWrite(quic.StreamErrorCode(ErrCodeFrameError))
 		return nil, fmt.Errorf("http3: HEADERS frame too large: %d bytes (max: %d)", hf.Length, s.maxHeaderBytes)
 	}
 	headerBlock := make([]byte, hf.Length)
 	if _, err := io.ReadFull(s.Stream, headerBlock); err != nil {
-		s.Stream.CancelRead(quic.StreamErrorCode(ErrCodeRequestIncomplete))
-		s.Stream.CancelWrite(quic.StreamErrorCode(ErrCodeRequestIncomplete))
+		s.CancelRead(quic.StreamErrorCode(ErrCodeRequestIncomplete))
+		s.CancelWrite(quic.StreamErrorCode(ErrCodeRequestIncomplete))
 		return nil, fmt.Errorf("http3: failed to read response headers: %w", err)
 	}
 	hfs, err := s.decoder.DecodeFull(headerBlock)
@@ -239,8 +239,8 @@ func (s *requestStream) ReadResponse() (*http.Response, error) {
 	}
 	res := s.response
 	if err := updateResponseFromHeaders(res, hfs); err != nil {
-		s.Stream.CancelRead(quic.StreamErrorCode(ErrCodeMessageError))
-		s.Stream.CancelWrite(quic.StreamErrorCode(ErrCodeMessageError))
+		s.CancelRead(quic.StreamErrorCode(ErrCodeMessageError))
+		s.CancelWrite(quic.StreamErrorCode(ErrCodeMessageError))
 		return nil, fmt.Errorf("http3: invalid response: %w", err)
 	}
 
diff --git a/http3/state_tracking_stream.go b/http3/state_tracking_stream.go
index 9cf17f5e..6edbeedf 100644
--- a/http3/state_tracking_stream.go
+++ b/http3/state_tracking_stream.go
@@ -90,7 +90,7 @@ func (s *stateTrackingStream) Close() error {
 }
 
 func (s *stateTrackingStream) CancelWrite(e quic.StreamErrorCode) {
-	s.closeSend(&quic.StreamError{StreamID: s.Stream.StreamID(), ErrorCode: e})
+	s.closeSend(&quic.StreamError{StreamID: s.StreamID(), ErrorCode: e})
 	s.Stream.CancelWrite(e)
 }
 
@@ -103,7 +103,7 @@ func (s *stateTrackingStream) Write(b []byte) (int, error) {
 }
 
 func (s *stateTrackingStream) CancelRead(e quic.StreamErrorCode) {
-	s.closeReceive(&quic.StreamError{StreamID: s.Stream.StreamID(), ErrorCode: e})
+	s.closeReceive(&quic.StreamError{StreamID: s.StreamID(), ErrorCode: e})
 	s.Stream.CancelRead(e)
 }
 
diff --git a/internal/flowcontrol/connection_flow_controller.go b/internal/flowcontrol/connection_flow_controller.go
index bbeb7889..c550e75a 100644
--- a/internal/flowcontrol/connection_flow_controller.go
+++ b/internal/flowcontrol/connection_flow_controller.go
@@ -61,8 +61,8 @@ func (c *connectionFlowController) AddBytesRead(n protocol.ByteCount) (hasWindow
 	c.mutex.Lock()
 	defer c.mutex.Unlock()
 
-	c.baseFlowController.addBytesRead(n)
-	return c.baseFlowController.hasWindowUpdate()
+	c.addBytesRead(n)
+	return c.hasWindowUpdate()
 }
 
 func (c *connectionFlowController) GetWindowUpdate(now time.Time) protocol.ByteCount {
@@ -70,7 +70,7 @@ func (c *connectionFlowController) GetWindowUpdate(now time.Time) protocol.ByteC
 	defer c.mutex.Unlock()
 
 	oldWindowSize := c.receiveWindowSize
-	offset := c.baseFlowController.getWindowUpdate(now)
+	offset := c.getWindowUpdate(now)
 	if c.logger.Debug() && oldWindowSize < c.receiveWindowSize {
 		c.logger.Debugf("Increasing receive flow control window for the connection to %d kB", c.receiveWindowSize/(1<<10))
 	}
diff --git a/internal/flowcontrol/stream_flow_controller.go b/internal/flowcontrol/stream_flow_controller.go
index ba005122..968e9a5b 100644
--- a/internal/flowcontrol/stream_flow_controller.go
+++ b/internal/flowcontrol/stream_flow_controller.go
@@ -100,7 +100,7 @@ func (c *streamFlowController) UpdateHighestReceived(offset protocol.ByteCount,
 
 func (c *streamFlowController) AddBytesRead(n protocol.ByteCount) (hasStreamWindowUpdate, hasConnWindowUpdate bool) {
 	c.mutex.Lock()
-	c.baseFlowController.addBytesRead(n)
+	c.addBytesRead(n)
 	hasStreamWindowUpdate = c.shouldQueueWindowUpdate()
 	c.mutex.Unlock()
 	hasConnWindowUpdate = c.connection.AddBytesRead(n)
@@ -145,7 +145,7 @@ func (c *streamFlowController) GetWindowUpdate(now time.Time) protocol.ByteCount
 	defer c.mutex.Unlock()
 
 	oldWindowSize := c.receiveWindowSize
-	offset := c.baseFlowController.getWindowUpdate(now)
+	offset := c.getWindowUpdate(now)
 	if c.receiveWindowSize > oldWindowSize { // auto-tuning enlarged the window size
 		c.logger.Debugf("Increasing receive flow control window for stream %d to %d", c.streamID, c.receiveWindowSize)
 		c.connection.EnsureMinimumWindowSize(protocol.ByteCount(float64(c.receiveWindowSize)*protocol.ConnectionFlowControlMultiplier), now)
diff --git a/internal/utils/buffered_write_closer.go b/internal/utils/buffered_write_closer.go
index b5b9d6fc..80ebfecb 100644
--- a/internal/utils/buffered_write_closer.go
+++ b/internal/utils/buffered_write_closer.go
@@ -19,7 +19,7 @@ func NewBufferedWriteCloser(writer *bufio.Writer, closer io.Closer) io.WriteClos
 }
 
 func (h bufferedWriteCloser) Close() error {
-	if err := h.Writer.Flush(); err != nil {
+	if err := h.Flush(); err != nil {
 		return err
 	}
 	return h.Closer.Close()
diff --git a/internal/wire/new_connection_id_frame.go b/internal/wire/new_connection_id_frame.go
index 852d46ef..6f2287f4 100644
--- a/internal/wire/new_connection_id_frame.go
+++ b/internal/wire/new_connection_id_frame.go
@@ -30,7 +30,7 @@ func parseNewConnectionIDFrame(b []byte, _ protocol.Version) (*NewConnectionIDFr
 	}
 	b = b[l:]
 	if ret > seq {
-		//nolint:stylecheck
+		//nolint:staticcheck // SA1021: Retire Prior To is the name of the field
 		return nil, 0, fmt.Errorf("Retire Prior To value (%d) larger than Sequence Number (%d)", ret, seq)
 	}
 	if len(b) == 0 {
diff --git a/internal/wire/version_negotiation.go b/internal/wire/version_negotiation.go
index a551aa8c..407cb629 100644
--- a/internal/wire/version_negotiation.go
+++ b/internal/wire/version_negotiation.go
@@ -16,11 +16,11 @@ func ParseVersionNegotiationPacket(b []byte) (dest, src protocol.ArbitraryLenCon
 	}
 	b = b[n:]
 	if len(b) == 0 {
-		//nolint:stylecheck
+		//nolint:staticcheck // SA1021: the packet is called Version Negotiation packet
 		return nil, nil, nil, errors.New("Version Negotiation packet has empty version list")
 	}
 	if len(b)%4 != 0 {
-		//nolint:stylecheck
+		//nolint:staticcheck // SA1021: the packet is called Version Negotiation packet
 		return nil, nil, nil, errors.New("Version Negotiation packet has a version list with an invalid length")
 	}
 	versions := make([]protocol.Version, len(b)/4)
diff --git a/metrics/tracer.go b/metrics/tracer.go
index 293291db..ffcdfd91 100644
--- a/metrics/tracer.go
+++ b/metrics/tracer.go
@@ -71,10 +71,11 @@ func NewTracerWithRegisterer(registerer prometheus.Registerer) *logging.Tracer {
 			defer putStringSlice(tags)
 
 			var reason string
-			switch {
-			case hdr.Type == protocol.PacketTypeRetry:
+			//nolint:exhaustive // we only care about Retry and Initial packets here
+			switch hdr.Type {
+			case protocol.PacketTypeRetry:
 				reason = "retry"
-			case hdr.Type == protocol.PacketTypeInitial:
+			case protocol.PacketTypeInitial:
 				var ccf *logging.ConnectionCloseFrame
 				for _, f := range frames {
 					cc, ok := f.(*logging.ConnectionCloseFrame)
diff --git a/packet_unpacker.go b/packet_unpacker.go
index 9e0fa9d9..2f607fbd 100644
--- a/packet_unpacker.go
+++ b/packet_unpacker.go
@@ -197,8 +197,7 @@ func (u *packetUnpacker) unpackLongHeader(hd headerDecryptor, hdr *wire.Header,
 func unpackLongHeader(hd headerDecryptor, hdr *wire.Header, data []byte) (*wire.ExtendedHeader, error) {
 	hdrLen := hdr.ParsedLen()
 	if protocol.ByteCount(len(data)) < hdrLen+4+16 {
-		//nolint:stylecheck
-		return nil, fmt.Errorf("Packet too small. Expected at least 20 bytes after the header, got %d", protocol.ByteCount(len(data))-hdrLen)
+		return nil, fmt.Errorf("packet too small, expected at least 20 bytes after the header, got %d", protocol.ByteCount(len(data))-hdrLen)
 	}
 	// The packet number can be up to 4 bytes long, but we won't know the length until we decrypt it.
 	// 1. save a copy of the 4 bytes
diff --git a/quicvarint/io.go b/quicvarint/io.go
index 9368d1c5..27def409 100644
--- a/quicvarint/io.go
+++ b/quicvarint/io.go
@@ -31,7 +31,7 @@ func NewReader(r io.Reader) Reader {
 
 func (r *byteReader) ReadByte() (byte, error) {
 	var b [1]byte
-	n, err := r.Reader.Read(b[:])
+	n, err := r.Read(b[:])
 	if n == 1 && err == io.EOF {
 		err = nil
 	}
@@ -63,6 +63,6 @@ func NewWriter(w io.Writer) Writer {
 }
 
 func (w *byteWriter) WriteByte(c byte) error {
-	_, err := w.Writer.Write([]byte{c})
+	_, err := w.Write([]byte{c})
 	return err
 }
diff --git a/sys_conn.go b/sys_conn.go
index 811131d9..6bfa3011 100644
--- a/sys_conn.go
+++ b/sys_conn.go
@@ -92,7 +92,7 @@ func (c *basicConn) ReadPacket() (receivedPacket, error) {
 	// The packet size should not exceed protocol.MaxPacketBufferSize bytes
 	// If it does, we only read a truncated packet, which will then end up undecryptable
 	buffer.Data = buffer.Data[:protocol.MaxPacketBufferSize]
-	n, addr, err := c.PacketConn.ReadFrom(buffer.Data)
+	n, addr, err := c.ReadFrom(buffer.Data)
 	if err != nil {
 		return receivedPacket{}, err
 	}
@@ -111,7 +111,7 @@ func (c *basicConn) WritePacket(b []byte, addr net.Addr, _ []byte, gsoSize uint1
 	if ecn != protocol.ECNUnsupported {
 		panic("cannot use ECN with a basicConn")
 	}
-	return c.PacketConn.WriteTo(b, addr)
+	return c.WriteTo(b, addr)
 }
 
 func (c *basicConn) capabilities() connCapabilities { return connCapabilities{DF: c.supportsDF} }
diff --git a/sys_conn_oob.go b/sys_conn_oob.go
index 75979682..5ed1b656 100644
--- a/sys_conn_oob.go
+++ b/sys_conn_oob.go
@@ -257,7 +257,7 @@ func (c *oobConn) WritePacket(b []byte, addr net.Addr, packetInfoOOB []byte, gso
 			}
 		}
 	}
-	n, _, err := c.OOBCapablePacketConn.WriteMsgUDP(b, oob, addr.(*net.UDPAddr))
+	n, _, err := c.WriteMsgUDP(b, oob, addr.(*net.UDPAddr))
 	return n, err
 }
 


Test patch:
diff --git a/integrationtests/self/close_test.go b/integrationtests/self/close_test.go
index 457f7bd9..e8e82487 100644
--- a/integrationtests/self/close_test.go
+++ b/integrationtests/self/close_test.go
@@ -131,7 +131,7 @@ func newBrokenConn(conn net.PacketConn) *brokenConn {
 	go func() {
 		<-c.broken
 		// make calls to ReadFrom return
-		c.PacketConn.SetDeadline(time.Now())
+		c.SetDeadline(time.Now())
 	}()
 	return c
 }
diff --git a/integrationtests/self/http_hotswap_test.go b/integrationtests/self/http_hotswap_test.go
index 65acfba8..638d8e24 100644
--- a/integrationtests/self/http_hotswap_test.go
+++ b/integrationtests/self/http_hotswap_test.go
@@ -50,7 +50,7 @@ func (ln *fakeClosingListener) Accept(ctx context.Context) (quic.EarlyConnection
 func (ln *fakeClosingListener) Close() error {
 	if ln.closed.CompareAndSwap(false, true) {
 		ln.cancel()
-		if ln.listenerWrapper.count.Add(-1) == 0 {
+		if ln.count.Add(-1) == 0 {
 			ln.listenerWrapper.Close()
 		}
 	}
diff --git a/integrationtests/self/multiplex_test.go b/integrationtests/self/multiplex_test.go
index 1a78c138..9437d311 100644
--- a/integrationtests/self/multiplex_test.go
+++ b/integrationtests/self/multiplex_test.go
@@ -326,10 +326,7 @@ func TestMultiplexingNonQUICPackets(t *testing.T) {
 	minExpected := numPackets * 4 / 5
 	timeout := time.After(time.Second)
 	var counter int
-	for {
-		if counter >= minExpected {
-			break
-		}
+	for counter < minExpected {
 		select {
 		case p := <-rcvdPackets:
 			require.Equal(t, tr1.Conn.LocalAddr(), p.addr, "non-QUIC packet received from wrong address")
diff --git a/integrationtests/self/zero_rtt_test.go b/integrationtests/self/zero_rtt_test.go
index 5453250f..87683d33 100644
--- a/integrationtests/self/zero_rtt_test.go
+++ b/integrationtests/self/zero_rtt_test.go
@@ -332,7 +332,7 @@ func Test0RTTWaitForHandshakeCompletion(t *testing.T) {
 	// check that 0-RTT packets only contain STREAM frames for the first stream
 	var num0RTT int
 	for _, p := range counter.getRcvdLongHeaderPackets() {
-		if p.hdr.Header.Type != protocol.PacketType0RTT {
+		if p.hdr.Type != protocol.PacketType0RTT {
 			continue
 		}
 		for _, f := range p.frames {
diff --git a/internal/qerr/errors_test.go b/internal/qerr/errors_test.go
index 44ab88e7..f217a48f 100644
--- a/internal/qerr/errors_test.go
+++ b/internal/qerr/errors_test.go
@@ -129,7 +129,7 @@ func TestHandshakeTimeoutError(t *testing.T) {
 	require.True(t, errors.Is(&HandshakeTimeoutError{}, &HandshakeTimeoutError{}))
 	require.False(t, errors.Is(&HandshakeTimeoutError{}, &IdleTimeoutError{}))
 
-	//nolint:gosimple // we need to assign to an interface here
+	//nolint:staticcheck // SA1021: we need to assign to an interface here
 	var err error
 	err = &HandshakeTimeoutError{}
 	nerr, ok := err.(net.Error)
@@ -143,7 +143,7 @@ func TestIdleTimeoutError(t *testing.T) {
 	require.True(t, errors.Is(&IdleTimeoutError{}, &IdleTimeoutError{}))
 	require.False(t, errors.Is(&IdleTimeoutError{}, &HandshakeTimeoutError{}))
 
-	//nolint:gosimple // we need to assign to an interface here
+	//nolint:staticcheck // SA1021: we need to assign to an interface here
 	var err error
 	err = &IdleTimeoutError{}
 	nerr, ok := err.(net.Error)
@@ -166,7 +166,7 @@ func TestStatelessResetError(t *testing.T) {
 	require.Equal(t, "received a stateless reset", (&StatelessResetError{}).Error())
 	require.True(t, errors.Is(&StatelessResetError{}, &StatelessResetError{}))
 
-	//nolint:gosimple // we need to assign to an interface here
+	//nolint:staticcheck // SA1021: we need to assign to an interface here
 	var err error
 	err = &StatelessResetError{}
 	nerr, ok := err.(net.Error)
diff --git a/packet_unpacker_test.go b/packet_unpacker_test.go
index f631c0d3..75f0db87 100644
--- a/packet_unpacker_test.go
+++ b/packet_unpacker_test.go
@@ -249,7 +249,7 @@ func TestUnpackHeaderSampleLongHeader(t *testing.T) {
 		cs.EXPECT().GetHandshakeOpener().Return(mocks.NewMockLongHeaderOpener(mockCtrl), nil)
 		_, err = unpacker.UnpackLongHeader(hdr, data[:len(data)-1])
 		require.IsType(t, &headerParseError{}, err)
-		require.ErrorContains(t, err, "Packet too small. Expected at least 20 bytes after the header, got 19")
+		require.ErrorContains(t, err, "packet too small, expected at least 20 bytes after the header, got 19")
 	})
 
 	t.Run("minimal size", func(t *testing.T) {
diff --git a/send_stream_test.go b/send_stream_test.go
index 9e743e93..f28b2d78 100644
--- a/send_stream_test.go
+++ b/send_stream_test.go
@@ -1012,10 +1012,7 @@ func TestSendStreamRetransmitDataUntilAcknowledged(t *testing.T) {
 	mockSender.EXPECT().onStreamCompleted(streamID).Do(func(protocol.StreamID) { completed = true })
 
 	received := make([]byte, dataLen)
-	for {
-		if completed {
-			break
-		}
+	for !completed {
 		f, _, _ := str.popStreamFrame(protocol.ByteCount(mrand.Intn(300)+100), protocol.Version1)
 		if f.Frame == nil {
 			continue