Repository: hertz-contrib/sse
Pull request #21: docs: update examples about end event check and errChan nil
Author: login: iyaozhen
Created: 2025-03-10T14:07:30Z | Merged: 2025-03-12T06:31:16Z

#### What type of PR is this?
<!--
Add one of the following kinds:

build: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
ci: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)
docs: Documentation only changes
feat: A new feature
optimize: A new optimization
fix: A bug fix
perf: A code change that improves performance
refactor: A code change that neither fixes a bug nor adds a feature
style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
test: Adding missing tests or correcting existing tests
chore: Changes to the build process or auxiliary tools and libraries such as documentation generation
-->

docs

#### What this PR does / why we need it (en: English/zh: Chinese):
<!--
The description will be attached in Release Notes, 
so please describe it from user-oriented.
-->
en: Optimization example code, about end event check and errChan nil
zh: 优化示例代码中，关于event是否结束的检查和errChan读到nil的处理

#### Which issue(s) this PR fixes:
<!--
*Automatically closes linked issue when PR is merged.
Eg: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
-->
Fixes https://github.com/hertz-contrib/sse/issues/20

另外发现一个别的情况，client1 subscribe cancel其实并没有生效，虽然ReadEvent里面处理了ctx.Done()，返回了io.EOF，但SubscribeWithContext中defer protocol.ReleaseResponse(resp)的时候需要等到response读完才能Release成功（初步debug看是卡在了CloseBodyStream）。
和这次改动没关系，但好像hertz特性如此？感觉不好修

运行日志：
```
21:16:06.136483 main.go:103: [Info] client2 http://127.0.0.1:8888/sse connect to server success with GET method
21:16:06.136484 main.go:57: [Info] client1 connect to server http://127.0.0.1:8888/sse success with GET method
21:16:06.136745 main.go:85: [Info] client1, &{Event:timestamp ID: Retry:0 Data:[xxx]}
21:16:06.136740 main.go:127: [Info] client2, &{Event:timestamp ID: Retry:0 Data:[xxx]}
21:16:07.137552 main.go:85: [Info] client1, &{Event:timestamp ID: Retry:0 Data:[xxx]}
21:16:08.137295 main.go:85: [Info] client1, &{Event:timestamp ID: Retry:0 Data:[xxx]}
21:16:08.137295 main.go:127: [Info] client2, &{Event:timestamp ID: Retry:0 Data:[xxx]}
21:16:09.137575 main.go:85: [Info] client1, &{Event:timestamp ID: Retry:0 Data:[xxx]}
21:16:10.135406 main.go:80: [Info] client1 subscribe cancel
21:16:10.137618 main.go:127: [Info] client2, &{Event:timestamp ID: Retry:0 Data:[xxx]}
21:16:12.137820 main.go:127: [Info] client2, &{Event:timestamp ID: Retry:0 Data:[xxx]}
21:16:14.138110 main.go:127: [Info] client2, &{Event:timestamp ID: Retry:0 Data:[xxx]}
21:16:15.137825 main.go:88: [Info] client1, ctx done, read stop    // 5s后才走到errChan <- cErr
21:16:16.138611 main.go:127: [Info] client2, &{Event:timestamp ID: Retry:0 Data:[xxx]}
21:16:18.139586 main.go:127: [Info] client2, &{Event:timestamp ID: Retry:0 Data:[xxx]}
21:16:20.140833 main.go:127: [Info] client2, &{Event:timestamp ID: Retry:0 Data:[xxx]}
21:16:22.141388 main.go:127: [Info] client2, &{Event:timestamp ID: Retry:0 Data:[xxx]}
21:16:24.142547 main.go:138: [Info] client2, stream closed   // 流关闭后，events channel里面还有内容
21:16:24.142771 main.go:127: [Info] client2, &{Event:timestamp ID: Retry:0 Data:[xxx]} 
21:16:26.143343 main.go:127: [Info] client2, &{Event:end ID: Retry:0 Data:[101 110 100 32 102 108 97 103]}
21:16:28.144210 main.go:147: [Info] client2, events is empty and stream closed
```


Diff:
diff --git a/.golangci.yaml b/.golangci.yaml
index c7358bb..e2fd2f6 100644
--- a/.golangci.yaml
+++ b/.golangci.yaml
@@ -1,15 +1,3 @@
-# Options for analysis running.
-run:
-  # include `vendor` `third_party` `testdata` `examples` `Godeps` `builtin`
-  skip-dirs-use-default: true
-  skip-dirs:
-    - kitex_gen
-  skip-files:
-    - ".*\\.mock\\.go$"
-# output configuration options
-output:
-  # Format: colored-line-number|line-number|json|tab|checkstyle|code-climate|junit-xml|github-actions
-  format: colored-line-number
 # All available settings of specific linters.
 # Refer to https://golangci-lint.run/usage/linters
 linters-settings:
diff --git a/README.md b/README.md
index 4755155..8a7e75c 100644
--- a/README.md
+++ b/README.md
@@ -23,6 +23,8 @@ go get github.com/hertz-contrib/sse
 
 ### Server
 
+see: [examples/server/quickstart/main.go](examples/server/quickstart/main.go)
+
 ```go
 package main
 
@@ -31,11 +33,11 @@ import (
   "net/http"
   "time"
 
-  "github.com/hertz-contrib/sse"
-
   "github.com/cloudwego/hertz/pkg/app"
   "github.com/cloudwego/hertz/pkg/app/server"
   "github.com/cloudwego/hertz/pkg/common/hlog"
+
+  "github.com/hertz-contrib/sse"
 )
 
 func main() {
@@ -49,6 +51,9 @@ func main() {
     // you must set status code and response headers before first render call
     c.SetStatusCode(http.StatusOK)
     s := sse.NewStream(c)
+
+    count := 0
+    sendCountLimit := 10
     for t := range time.NewTicker(1 * time.Second).C {
       event := &sse.Event{
         Event: "timestamp",
@@ -58,26 +63,40 @@ func main() {
       if err != nil {
         return
       }
+      count++
+      if count >= sendCountLimit {
+        // send end flag to client
+        err := s.Publish(&sse.Event{
+          Event: "end",
+          Data:  []byte("end flag"),
+        })
+        if err != nil {
+          return
+        }
+        break
+      }
     }
   })
 
   h.Spin()
 }
-
 ```
 
 ### Client
 
+see: [examples/client/quickstart/main.go](examples/client/quickstart/main.go)
+
 ```go
 package main
 
 import (
   "context"
   "sync"
-
-  "github.com/hertz-contrib/sse"
+  "time"
 
   "github.com/cloudwego/hertz/pkg/common/hlog"
+
+  "github.com/hertz-contrib/sse"
 )
 
 var wg sync.WaitGroup
@@ -85,7 +104,6 @@ var wg sync.WaitGroup
 func main() {
   wg.Add(2)
   go func() {
-    // pass in the server-side URL to initialize the client	  
     c := sse.NewClient("http://127.0.0.1:8888/sse")
 
     // touch off when connected to the server
@@ -100,8 +118,9 @@ func main() {
 
     events := make(chan *sse.Event)
     errChan := make(chan error)
+    ctx, cancel := context.WithCancel(context.Background())
     go func() {
-      cErr := c.Subscribe(func(msg *sse.Event) {
+      cErr := c.SubscribeWithContext(ctx, func(msg *sse.Event) {
         if msg.Data != nil {
           events <- msg
           return
@@ -109,12 +128,21 @@ func main() {
       })
       errChan <- cErr
     }()
+    go func() {
+      time.Sleep(5 * time.Second)
+      cancel()
+      hlog.Info("client1 subscribe cancel")
+    }()
     for {
       select {
       case e := <-events:
-        hlog.Info(e)
+        hlog.Infof("client1, %+v", e)
       case err := <-errChan:
-        hlog.CtxErrorf(context.Background(), "err = %s", err.Error())
+        if err == nil {
+          hlog.Info("client1, ctx done, read stop")
+        } else {
+          hlog.CtxErrorf(ctx, "client1, err = %s", err.Error())
+        }
         wg.Done()
         return
       }
@@ -122,7 +150,6 @@ func main() {
   }()
 
   go func() {
-    // pass in the server-side URL to initialize the client	  
     c := sse.NewClient("http://127.0.0.1:8888/sse")
 
     // touch off when connected to the server
@@ -135,10 +162,10 @@ func main() {
       hlog.Infof("client2 %s disconnect to server success with %s method", c.GetURL(), c.GetMethod())
     })
 
-    events := make(chan *sse.Event)
+    events := make(chan *sse.Event, 10)
     errChan := make(chan error)
     go func() {
-      cErr := c.Subscribe( func(msg *sse.Event) {
+      cErr := c.Subscribe(func(msg *sse.Event) {
         if msg.Data != nil {
           events <- msg
           return
@@ -146,14 +173,35 @@ func main() {
       })
       errChan <- cErr
     }()
+
+    streamClosed := false
     for {
       select {
       case e := <-events:
-        hlog.Info(e)
+        hlog.Infof("client2, %+v", e)
+        time.Sleep(2 * time.Second) // do something blocked
+        // When the event ends, you should break out of the loop.
+        if checkEventEnd(e) {
+          wg.Done()
+          return
+        }
       case err := <-errChan:
-        hlog.CtxErrorf(context.Background(), "err = %s", err.Error())
+        if err == nil {
+          // err is nil means read io.EOF, stream is closed
+          streamClosed = true
+          hlog.Info("client2, stream closed")
+          // continue read channel events
+          continue
+        }
+        hlog.CtxErrorf(context.Background(), "client2, err = %s", err.Error())
         wg.Done()
         return
+      default:
+        if streamClosed {
+          hlog.Info("client2, events is empty and stream closed")
+          wg.Done()
+          return
+        }
       }
     }
   }()
@@ -161,6 +209,10 @@ func main() {
   wg.Wait()
 }
 
+func checkEventEnd(e *sse.Event) bool {
+  // check e.Data or e.Event. It depends on the definition of the server
+  return e.Event == "end" || string(e.Data) == "end flag"
+}
 ```
 
 ## Real-world examples
diff --git a/README_CN.md b/README_CN.md
index 5634352..5796f07 100644
--- a/README_CN.md
+++ b/README_CN.md
@@ -21,144 +21,196 @@ go get github.com/hertz-contrib/sse
 
 ### 服务端
 
+查看: [examples/server/quickstart/main.go](examples/server/quickstart/main.go)
+
 ```go
 package main
 
 import (
-  "context"
-  "net/http"
-  "time"
+	"context"
+	"net/http"
+	"time"
 
-  "github.com/hertz-contrib/sse"
+	"github.com/cloudwego/hertz/pkg/app"
+	"github.com/cloudwego/hertz/pkg/app/server"
+	"github.com/cloudwego/hertz/pkg/common/hlog"
 
-  "github.com/cloudwego/hertz/pkg/app"
-  "github.com/cloudwego/hertz/pkg/app/server"
-  "github.com/cloudwego/hertz/pkg/common/hlog"
+	"github.com/hertz-contrib/sse"
 )
 
 func main() {
-  h := server.Default()
-
-  h.GET("/sse", func(ctx context.Context, c *app.RequestContext) {
-    // 客户端可以通过 Last-Event-ID 标头告诉服务端它收到的最后一个事件
-    lastEventID := sse.GetLastEventID(c)
-    hlog.CtxInfof(ctx, "last event ID: %s", lastEventID)
-
-    // 必须在第一次调用之前设置状态代码和响应标头
-    c.SetStatusCode(http.StatusOK)
-    s := sse.NewStream(c)
-    for t := range time.NewTicker(1 * time.Second).C {
-      event := &sse.Event{
-        Event: "timestamp",
-        Data:  []byte(t.Format(time.RFC3339)),
-      }
-      err := s.Publish(event)
-      if err != nil {
-        return
-      }
-    }
-  })
-
-  h.Spin()
+	h := server.Default()
+
+	h.GET("/sse", func(ctx context.Context, c *app.RequestContext) {
+		// 客户端可以通过 Last-Event-ID 标头告诉服务端它收到的最后一个事件
+		lastEventID := sse.GetLastEventID(c)
+		hlog.CtxInfof(ctx, "last event ID: %s", lastEventID)
+
+		// 必须在第一次调用之前设置状态代码和响应标头
+		c.SetStatusCode(http.StatusOK)
+		s := sse.NewStream(c)
+
+		count := 0
+		sendCountLimit := 10
+		for t := range time.NewTicker(1 * time.Second).C {
+			event := &sse.Event{
+				Event: "timestamp",
+				Data:  []byte(t.Format(time.RFC3339)),
+			}
+			err := s.Publish(event)
+			if err != nil {
+				return
+			}
+			count++
+			if count >= sendCountLimit {
+				// 发送结束标识到客户端
+				err := s.Publish(&sse.Event{
+					Event: "end",
+					Data:  []byte("end flag"),
+				})
+				if err != nil {
+					return
+				}
+				break
+			}
+		}
+	})
+
+	h.Spin()
 }
-
 ```
 
 ### 客户端
 
+查看: [examples/client/quickstart/main.go](examples/client/quickstart/main.go)
+
 ```go
 package main
 
 import (
 	"context"
 	"sync"
-
-	"github.com/hertz-contrib/sse"
+	"time"
 
 	"github.com/cloudwego/hertz/pkg/common/hlog"
+
+	"github.com/hertz-contrib/sse"
 )
 
 var wg sync.WaitGroup
 
 func main() {
-  wg.Add(2)	
-  go func() {
-    // 传入 server 端 URL 初始化客户端  	  
-    c := sse.NewClient("http://127.0.0.1:8888/sse")
-
-    // 连接到服务端的时候触发
-    c.SetOnConnectCallback(func(ctx context.Context, client *sse.Client) {
-      hlog.Infof("client1 connect to server %s success with %s method", c.GetURL(), c.GetMethod())
-    })
-
-    // 服务端断开连接的时候触发
-    c.SetDisconnectCallback(func(ctx context.Context, client *sse.Client) {
-      hlog.Infof("client1 disconnect to server %s success with %s method", c.GetURL(), c.GetMethod())
-    })
-
-    events := make(chan *sse.Event)
-    errChan := make(chan error)
-    go func() {
-      cErr := c.Subscribe(func(msg *sse.Event) {
-        if msg.Data != nil {
-          events <- msg
-          return
-        }
-      })
-      errChan <- cErr
-    }()
-    for {
-      select {
-      case e := <-events:
-        hlog.Info(e)
-      case err := <-errChan:
-        hlog.CtxErrorf(context.Background(), "err = %s", err.Error())
-		wg.Done()
-        return
-      }
-    }
-  }()
-
-  go func() {
-    // 传入 server 端 URL 初始化客户端  
-    c := sse.NewClient("http://127.0.0.1:8888/sse")
-
-    // 连接到服务端的时候触发
-    c.SetOnConnectCallback(func(ctx context.Context, client *sse.Client) {
-      hlog.Infof("client2 %s connect to server success with %s method",c.GetURL(), c.GetMethod())
-    })
-
-    // 服务端断开连接的时候触发
-    c.SetDisconnectCallback(func(ctx context.Context, client *sse.Client) {
-      hlog.Infof("client2 %s disconnect to server success with %s method", c.GetURL(), c.GetMethod())
-    })
-
-    events := make(chan *sse.Event)
-    errChan := make(chan error)
-    go func() {
-      cErr := c.Subscribe(func(msg *sse.Event) {
-        if msg.Data != nil {
-          events <- msg
-          return
-        }
-      })
-      errChan <- cErr
-    }()
-    for {
-      select {
-      case e := <-events:
-        hlog.Info(e)
-      case err := <-errChan:
-        hlog.CtxErrorf(context.Background(), "err = %s", err.Error())
-		wg.Done()
-        return
-      }
-    }
-  }()
-
-  wg.Wait()
+	wg.Add(2)
+	go func() {
+		c := sse.NewClient("http://127.0.0.1:8888/sse")
+
+		// 连接到服务端的时候触发
+		c.SetOnConnectCallback(func(ctx context.Context, client *sse.Client) {
+			hlog.Infof("client1 connect to server %s success with %s method", c.GetURL(), c.GetMethod())
+		})
+
+		// 服务端断开连接的时候触发
+		c.SetDisconnectCallback(func(ctx context.Context, client *sse.Client) {
+			hlog.Infof("client1 disconnect to server %s success with %s method", c.GetURL(), c.GetMethod())
+		})
+
+		events := make(chan *sse.Event)
+		errChan := make(chan error)
+		ctx, cancel := context.WithCancel(context.Background())
+		go func() {
+			cErr := c.SubscribeWithContext(ctx, func(msg *sse.Event) {
+				if msg.Data != nil {
+					events <- msg
+					return
+				}
+			})
+			errChan <- cErr
+		}()
+		go func() {
+			time.Sleep(5 * time.Second)
+			cancel()
+			hlog.Info("client1 subscribe cancel")
+		}()
+		for {
+			select {
+			case e := <-events:
+				hlog.Infof("client1, %+v", e)
+			case err := <-errChan:
+				if err == nil {
+					hlog.Info("client1, ctx done, read stop")
+				} else {
+					hlog.CtxErrorf(ctx, "client1, err = %s", err.Error())
+				}
+				wg.Done()
+				return
+			}
+		}
+	}()
+
+	go func() {
+		c := sse.NewClient("http://127.0.0.1:8888/sse")
+
+		// 连接到服务端的时候触发
+		c.SetOnConnectCallback(func(ctx context.Context, client *sse.Client) {
+			hlog.Infof("client2 %s connect to server success with %s method", c.GetURL(), c.GetMethod())
+		})
+
+		// 服务端断开连接的时候触发
+		c.SetDisconnectCallback(func(ctx context.Context, client *sse.Client) {
+			hlog.Infof("client2 %s disconnect to server success with %s method", c.GetURL(), c.GetMethod())
+		})
+
+		events := make(chan *sse.Event, 10)
+		errChan := make(chan error)
+		go func() {
+			cErr := c.Subscribe(func(msg *sse.Event) {
+				if msg.Data != nil {
+					events <- msg
+					return
+				}
+			})
+			errChan <- cErr
+		}()
+
+		streamClosed := false
+		for {
+			select {
+			case e := <-events:
+				hlog.Infof("client2, %+v", e)
+				time.Sleep(2 * time.Second) // 业务逻辑，阻塞
+				// 如果这个event是结束包，应该直接跳出循环
+				if checkEventEnd(e) {
+					wg.Done()
+					return
+				}
+			case err := <-errChan:
+				if err == nil {
+					// err 是 nil 表示读到了 io.EOF，流已经结束了
+					streamClosed = true
+					hlog.Info("client2, stream closed")
+					// 继续读取 events 通道
+					continue
+				}
+				hlog.CtxErrorf(context.Background(), "client2, err = %s", err.Error())
+				wg.Done()
+				return
+			default:
+				if streamClosed {
+					hlog.Info("client2, events is empty and stream closed")
+					wg.Done()
+					return
+				}
+			}
+		}
+	}()
+
+	wg.Wait()
 }
 
+func checkEventEnd(e *sse.Event) bool {
+	// 可以检查 e.Data 或者 e.Event, 取决于服务端的定义
+	return e.Event == "end" || string(e.Data) == "end flag"
+}
 ```
 
 ## 真实场景示例
diff --git a/examples/client/quickstart/main.go b/examples/client/quickstart/main.go
index 928edd0..30310d6 100644
--- a/examples/client/quickstart/main.go
+++ b/examples/client/quickstart/main.go
@@ -38,11 +38,12 @@ package main
 
 import (
 	"context"
-	"fmt"
-	"github.com/cloudwego/hertz/pkg/common/hlog"
-	"github.com/hertz-contrib/sse"
 	"sync"
 	"time"
+
+	"github.com/cloudwego/hertz/pkg/common/hlog"
+
+	"github.com/hertz-contrib/sse"
 )
 
 var wg sync.WaitGroup
@@ -77,14 +78,18 @@ func main() {
 		go func() {
 			time.Sleep(5 * time.Second)
 			cancel()
-			fmt.Println("client1 subscribe cancel")
+			hlog.Info("client1 subscribe cancel")
 		}()
 		for {
 			select {
 			case e := <-events:
-				hlog.Info(e)
+				hlog.Infof("client1, %+v", e)
 			case err := <-errChan:
-				hlog.CtxErrorf(context.Background(), "err = %s", err.Error())
+				if err == nil {
+					hlog.Info("client1, ctx done, read stop")
+				} else {
+					hlog.CtxErrorf(ctx, "client1, err = %s", err.Error())
+				}
 				wg.Done()
 				return
 			}
@@ -104,7 +109,7 @@ func main() {
 			hlog.Infof("client2 %s disconnect to server success with %s method", c.GetURL(), c.GetMethod())
 		})
 
-		events := make(chan *sse.Event)
+		events := make(chan *sse.Event, 10)
 		errChan := make(chan error)
 		go func() {
 			cErr := c.Subscribe(func(msg *sse.Event) {
@@ -115,17 +120,43 @@ func main() {
 			})
 			errChan <- cErr
 		}()
+
+		streamClosed := false
 		for {
 			select {
 			case e := <-events:
-				hlog.Info(e)
+				hlog.Infof("client2, %+v", e)
+				time.Sleep(2 * time.Second) // do something blocked
+				// When the event ends, you should break out of the loop.
+				if checkEventEnd(e) {
+					wg.Done()
+					return
+				}
 			case err := <-errChan:
-				hlog.CtxErrorf(context.Background(), "err = %s", err.Error())
+				if err == nil {
+					// err is nil means read io.EOF, stream is closed
+					streamClosed = true
+					hlog.Info("client2, stream closed")
+					// continue read channel events
+					continue
+				}
+				hlog.CtxErrorf(context.Background(), "client2, err = %s", err.Error())
 				wg.Done()
 				return
+			default:
+				if streamClosed {
+					hlog.Info("client2, events is empty and stream closed")
+					wg.Done()
+					return
+				}
 			}
 		}
 	}()
 
 	wg.Wait()
 }
+
+func checkEventEnd(e *sse.Event) bool {
+	// check e.Data or e.Event. It depends on the definition of the server
+	return e.Event == "end" || string(e.Data) == "end flag"
+}
diff --git a/examples/server/quickstart/main.go b/examples/server/quickstart/main.go
index a384107..b189af2 100644
--- a/examples/server/quickstart/main.go
+++ b/examples/server/quickstart/main.go
@@ -41,11 +41,11 @@ import (
 	"net/http"
 	"time"
 
-	"github.com/hertz-contrib/sse"
-
 	"github.com/cloudwego/hertz/pkg/app"
 	"github.com/cloudwego/hertz/pkg/app/server"
 	"github.com/cloudwego/hertz/pkg/common/hlog"
+
+	"github.com/hertz-contrib/sse"
 )
 
 func main() {
@@ -59,6 +59,9 @@ func main() {
 		// you must set status code and response headers before first render call
 		c.SetStatusCode(http.StatusOK)
 		s := sse.NewStream(c)
+
+		count := 0
+		sendCountLimit := 10
 		for t := range time.NewTicker(1 * time.Second).C {
 			event := &sse.Event{
 				Event: "timestamp",
@@ -68,6 +71,18 @@ func main() {
 			if err != nil {
 				return
 			}
+			count++
+			if count >= sendCountLimit {
+				// send end flag to client
+				err := s.Publish(&sse.Event{
+					Event: "end",
+					Data:  []byte("end flag"),
+				})
+				if err != nil {
+					return
+				}
+				break
+			}
 		}
 	})
 


Reviews:
user: login: DMwangnima
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-11T10:03:04Z
user: login: iyaozhen
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T03:32:16Z
user: login: DMwangnima
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-12T05:44:55Z
user: login: iyaozhen
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T06:24:50Z
user: login: DMwangnima
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-12T06:30:17Z

Review comments:
diff_hunk: @@ -135,32 +162,57 @@ func main() {
       hlog.Infof("client2 %s disconnect to server success with %s method", c.GetURL(), c.GetMethod())
     })
 
-    events := make(chan *sse.Event)
+    events := make(chan *sse.Event, 10)
     errChan := make(chan error)
     go func() {
-      cErr := c.Subscribe( func(msg *sse.Event) {
+      cErr := c.Subscribe(func(msg *sse.Event) {
         if msg.Data != nil {
           events <- msg
           return
         }
       })
       errChan <- cErr
     }()
+
+    streamClosed := false
     for {
       select {
       case e := <-events:
-        hlog.Info(e)
+        hlog.Infof("client2, %+v", e)
+        time.Sleep(2 * time.Second) // do something blocked
+        // When the event ends, you should break out of the loop.
+        if checkEventEnd(e) {
+          wg.Done()
+          return
+        }
       case err := <-errChan:
-        hlog.CtxErrorf(context.Background(), "err = %s", err.Error())
+        if err == nil {
+          // err is nil means read io.EOF, stream is closed
+          streamClosed = true
+          hlog.Info("client2, stream closed")
+          // continue read channel events
+          continue
+        }
+        hlog.CtxErrorf(context.Background(), "client2, err = %s", err.Error())
         wg.Done()
         return
+      default:
+        if streamClosed {
+          hlog.Info("client2, events is empty and stream closed")
+          wg.Done()
+          return
+        }
       }
     }
   }()
 
   wg.Wait()
 }
 
+func checkEventEnd(e *sse.Event) bool {
+  // check e.Data or e.Event
+  return false
path: README.md
user: login: DMwangnima
body: 这里忘了对 Event 做判断？
created_at: 2025-03-11T10:03:04Z
author_association: COLLABORATOR
diff_hunk: @@ -135,32 +162,57 @@ func main() {
       hlog.Infof("client2 %s disconnect to server success with %s method", c.GetURL(), c.GetMethod())
     })
 
-    events := make(chan *sse.Event)
+    events := make(chan *sse.Event, 10)
     errChan := make(chan error)
     go func() {
-      cErr := c.Subscribe( func(msg *sse.Event) {
+      cErr := c.Subscribe(func(msg *sse.Event) {
         if msg.Data != nil {
           events <- msg
           return
         }
       })
       errChan <- cErr
     }()
+
+    streamClosed := false
     for {
       select {
       case e := <-events:
-        hlog.Info(e)
+        hlog.Infof("client2, %+v", e)
+        time.Sleep(2 * time.Second) // do something blocked
+        // When the event ends, you should break out of the loop.
+        if checkEventEnd(e) {
+          wg.Done()
+          return
+        }
       case err := <-errChan:
-        hlog.CtxErrorf(context.Background(), "err = %s", err.Error())
+        if err == nil {
+          // err is nil means read io.EOF, stream is closed
+          streamClosed = true
+          hlog.Info("client2, stream closed")
+          // continue read channel events
+          continue
+        }
+        hlog.CtxErrorf(context.Background(), "client2, err = %s", err.Error())
         wg.Done()
         return
+      default:
+        if streamClosed {
+          hlog.Info("client2, events is empty and stream closed")
+          wg.Done()
+          return
+        }
       }
     }
   }()
 
   wg.Wait()
 }
 
+func checkEventEnd(e *sse.Event) bool {
+  // check e.Data or e.Event
+  return false
path: README.md
user: login: iyaozhen
body: 我是想留给用户自己写。如果用户想简单点（不实现check），也能靠兜底的 errChan nil 自然结束。
我也有点纠结，如果你觉得加上好点那我改下
created_at: 2025-03-12T03:32:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -135,32 +162,57 @@ func main() {
       hlog.Infof("client2 %s disconnect to server success with %s method", c.GetURL(), c.GetMethod())
     })
 
-    events := make(chan *sse.Event)
+    events := make(chan *sse.Event, 10)
     errChan := make(chan error)
     go func() {
-      cErr := c.Subscribe( func(msg *sse.Event) {
+      cErr := c.Subscribe(func(msg *sse.Event) {
         if msg.Data != nil {
           events <- msg
           return
         }
       })
       errChan <- cErr
     }()
+
+    streamClosed := false
     for {
       select {
       case e := <-events:
-        hlog.Info(e)
+        hlog.Infof("client2, %+v", e)
+        time.Sleep(2 * time.Second) // do something blocked
+        // When the event ends, you should break out of the loop.
+        if checkEventEnd(e) {
+          wg.Done()
+          return
+        }
       case err := <-errChan:
-        hlog.CtxErrorf(context.Background(), "err = %s", err.Error())
+        if err == nil {
+          // err is nil means read io.EOF, stream is closed
+          streamClosed = true
+          hlog.Info("client2, stream closed")
+          // continue read channel events
+          continue
+        }
+        hlog.CtxErrorf(context.Background(), "client2, err = %s", err.Error())
         wg.Done()
         return
+      default:
+        if streamClosed {
+          hlog.Info("client2, events is empty and stream closed")
+          wg.Done()
+          return
+        }
       }
     }
   }()
 
   wg.Wait()
 }
 
+func checkEventEnd(e *sse.Event) bool {
+  // check e.Data or e.Event
+  return false
path: README.md
user: login: DMwangnima
body: server 侧已经提供了 end event 的示例，这里可以直接判断并且写上注释，标明可以自行选择其它方式判断是否结束
created_at: 2025-03-12T05:44:55Z
author_association: COLLABORATOR
diff_hunk: @@ -135,32 +162,57 @@ func main() {
       hlog.Infof("client2 %s disconnect to server success with %s method", c.GetURL(), c.GetMethod())
     })
 
-    events := make(chan *sse.Event)
+    events := make(chan *sse.Event, 10)
     errChan := make(chan error)
     go func() {
-      cErr := c.Subscribe( func(msg *sse.Event) {
+      cErr := c.Subscribe(func(msg *sse.Event) {
         if msg.Data != nil {
           events <- msg
           return
         }
       })
       errChan <- cErr
     }()
+
+    streamClosed := false
     for {
       select {
       case e := <-events:
-        hlog.Info(e)
+        hlog.Infof("client2, %+v", e)
+        time.Sleep(2 * time.Second) // do something blocked
+        // When the event ends, you should break out of the loop.
+        if checkEventEnd(e) {
+          wg.Done()
+          return
+        }
       case err := <-errChan:
-        hlog.CtxErrorf(context.Background(), "err = %s", err.Error())
+        if err == nil {
+          // err is nil means read io.EOF, stream is closed
+          streamClosed = true
+          hlog.Info("client2, stream closed")
+          // continue read channel events
+          continue
+        }
+        hlog.CtxErrorf(context.Background(), "client2, err = %s", err.Error())
         wg.Done()
         return
+      default:
+        if streamClosed {
+          hlog.Info("client2, events is empty and stream closed")
+          wg.Done()
+          return
+        }
       }
     }
   }()
 
   wg.Wait()
 }
 
+func checkEventEnd(e *sse.Event) bool {
+  // check e.Data or e.Event
+  return false
path: README.md
user: login: iyaozhen
body: 更新了`checkEventEnd`的实现，更贴合实际场景
created_at: 2025-03-12T06:24:50Z
author_association: CONTRIBUTOR

PR comments:
user: login: CLAassistant
created_at: 2025-03-10T14:07:36Z
body: [![CLA assistant check](https://cla-assistant.io/pull/badge/signed)](https://cla-assistant.io/hertz-contrib/sse?pullRequest=21) <br/>All committers have signed the CLA.
author_association: NONE
user: login: iyaozhen
created_at: 2025-03-11T02:50:33Z
body: 参考另一个PR修改了下 .golangci.yaml 文件
author_association: CONTRIBUTOR