A Stream has four methods. The loop is three lines. The two lines that are
easy to omit are the two that matter most.
You will learn
- What each of the four methods does
- Why
defer stream.Close()is required even though the reader is context-bound - Why
stream.Err()after the loop is not optional - What is retried on a stream, and what is not
The interface#
type Stream interface {
Next() bool // advance; false at end of stream AND on error
Event() StreamEvent // valid only after Next returned true
Err() error // the error that stopped it, or nil
Close() error // release resources; safe to call twice, and early
}type Stream interface {
Next() bool // advance; false at end of stream AND on error
Event() StreamEvent // valid only after Next returned true
Err() error // the error that stopped it, or nil
Close() error // release resources; safe to call twice, and early
}The loop#
stream, err := client.Stream(ctx, req)
if err != nil {
return err
}
defer stream.Close() // ← line one
for stream.Next() {
ev := stream.Event()
switch ev.Type {
case skyl.EventTextDelta:
fmt.Print(ev.Text)
case skyl.EventDone:
if ev.Usage != nil {
log.Printf("%d tokens", ev.Usage.TotalTokens())
}
}
}
return stream.Err() // ← line twostream, err := client.Stream(ctx, req)
if err != nil {
return err
}
defer stream.Close() // ← line one
for stream.Next() {
ev := stream.Event()
switch ev.Type {
case skyl.EventTextDelta:
fmt.Print(ev.Text)
case skyl.EventDone:
if ev.Usage != nil {
log.Printf("%d tokens", ev.Usage.TotalTokens())
}
}
}
return stream.Err() // ← line twoLine one: defer Close#
Close is safe to call more than once, and safe to call before the stream is
exhausted, so defer is always correct.
Line two: check Err#
What is retried#
Only the handshake. Once bytes are flowing, a mid-stream failure surfaces
through Err() rather than being retried.
Deep diveWhy not retry a stream that fails halfway?
Because you have already seen output. Replaying a partially consumed response would duplicate the text the caller printed — a user watching tokens appear would see the answer restart mid-sentence.
The alternative, buffering everything and only emitting once complete, defeats the purpose of streaming.
So Client.Stream retries the initial call — a 429 or a 503 on the handshake is
retried with backoff, exactly like Complete — and hands everything after that
to you. If your use case can tolerate a restart, you can retry at your own
level, where you know whether output has been shown.
A stream is not concurrency-safe#
Client and every adapter are safe for concurrent use. A Stream is not.
One stream, one consuming goroutine. If you need to fan the events out, read
them in one goroutine and publish onto a channel yourself.
Recap
- Four methods:
Next,Event,Err,Close. defer stream.Close()— abandoning is safe, butClosemakes release prompt.- Always check
stream.Err()—Nextreturning false does not mean success. - Only the handshake is retried; mid-stream failures surface through
Err. - One stream, one goroutine.
Clientis concurrency-safe;Streamis not.
Try out some challenges
Each of these is solvable with what this page covered. Run them against the sandbox — no API key needed.
Stream with a first-token deadline
Give the model 5 seconds to produce its first token, then let it take as long as it needs for the rest.
Show hint
You cannot use one context for both. Cancel a timer when the first delta arrives.
Show solution
ctx, cancel := context.WithCancel(ctx)
defer cancel()
stream, err := client.Stream(ctx, req)
if err != nil {
return err
}
defer stream.Close()
// Cancel the whole stream if nothing arrives within the deadline.
firstToken := make(chan struct{})
go func() {
select {
case <-firstToken:
case <-time.After(5 * time.Second):
cancel()
case <-ctx.Done():
}
}()
var seen bool
for stream.Next() {
if !seen {
close(firstToken)
seen = true
}
if ev := stream.Event(); ev.Type == skyl.EventTextDelta {
fmt.Print(ev.Text)
}
}
return stream.Err()ctx, cancel := context.WithCancel(ctx)
defer cancel()
stream, err := client.Stream(ctx, req)
if err != nil {
return err
}
defer stream.Close()
// Cancel the whole stream if nothing arrives within the deadline.
firstToken := make(chan struct{})
go func() {
select {
case <-firstToken:
case <-time.After(5 * time.Second):
cancel()
case <-ctx.Done():
}
}()
var seen bool
for stream.Next() {
if !seen {
close(firstToken)
seen = true
}
if ev := stream.Event(); ev.Type == skyl.EventTextDelta {
fmt.Print(ev.Text)
}
}
return stream.Err()The ctx.Done() case in the select stops the goroutine leaking when the stream
ends normally — which matters, because this pattern runs once per request.