You have a working Go service and you want to add a feature that summarizes user input or generates code. You grab your OpenAI API key, read the documentation, and realize you need to construct JSON payloads, attach bearer tokens, handle HTTP status codes, and parse nested response objects. Writing that HTTP boilerplate from scratch is tedious. The community standard is to use a typed client that handles the serialization and authentication for you.
How the API actually works
The OpenAI API is fundamentally a REST endpoint that accepts JSON and returns JSON. Under the hood, your program opens a TCP connection, sends an HTTP POST request with an Authorization: Bearer header, waits for the response, and decodes the result. The github.com/sashabaranov/go-openai package wraps this cycle in Go structs and methods. Think of it like a pre-assembled circuit board. You still need to connect power and write the logic, but you skip soldering every individual wire. The library gives you type safety, so the compiler catches mismatched fields before the request ever leaves your machine.
Go favors explicit interfaces and concrete types. The library exposes ChatCompletionRequest and ChatCompletionResponse structs that map directly to the JSON schema OpenAI publishes. When you fill out a struct, the library marshals it to JSON, attaches the correct content type, and sends it. When the response arrives, the library unmarshals the JSON back into a struct. You never write json.Marshal or json.Unmarshal manually. The compiler enforces that you provide a model name and a message array. Missing fields result in zero values, which the API usually rejects with a clear validation error.
Type safety prevents silent failures. If OpenAI changes a field name or adds a required parameter, your code fails to compile or returns a predictable error instead of sending malformed JSON to a paid endpoint. Trust the type system. Wrap the value or change the design.
Minimal example
Here is the simplest way to send a prompt and get a reply.
package main
import (
"context"
"fmt"
"log"
"os"
"github.com/sashabaranov/go-openai"
)
func main() {
// Load the key from environment to avoid hardcoding secrets
apiKey := os.Getenv("OPENAI_API_KEY")
if apiKey == "" {
log.Fatal("OPENAI_API_KEY environment variable is not set")
}
// Initialize the client with the key
client := openai.NewClient(apiKey)
// Create a context for cancellation and timeout control
ctx := context.Background()
// Build the request with model and message parameters
resp, err := client.CreateChatCompletion(ctx, openai.ChatCompletionRequest{
Model: openai.GPT3Dot5Turbo,
Messages: []openai.ChatCompletionMessage{
{Role: openai.ChatMessageRoleUser, Content: "Explain goroutines in one sentence."},
},
})
// Handle network or API errors before accessing the response
if err != nil {
log.Fatal(err)
}
// Safely extract the first choice and print the content
if len(resp.Choices) > 0 {
fmt.Println(resp.Choices[0].Message.Content)
}
}
Walkthrough of the request lifecycle
The program starts by reading the API key from the environment. Hardcoding credentials is a security risk, and the library expects a plain string. The NewClient function stores the key and configures an underlying HTTP client with default timeouts. You pass context.Background() as the first argument to CreateChatCompletion. Go convention dictates that context always travels first in function signatures, usually named ctx. This allows the library to respect cancellation signals and deadlines. The request struct maps directly to the JSON payload OpenAI expects. When you call the method, the library marshals the struct, attaches the authorization header, sends the request, and unmarshals the JSON response back into a typed struct. If the network fails or OpenAI returns a non-200 status, the error object contains the HTTP status and any error message from the provider.
The if err != nil check is verbose by design. The Go community accepts this boilerplate because it forces you to acknowledge failure paths instead of hiding them. You can wrap the error with fmt.Errorf("request failed: %w", err) to preserve the original chain for debugging. The library does not swallow errors. It returns them exactly as the HTTP transport or the API provides them.
Context propagation is the backbone of Go concurrency. When you pass ctx to the client, the library registers a deadline or cancellation channel. If the parent context cancels, the HTTP request aborts immediately. This prevents hanging connections when a user closes their browser or a downstream service times out. Always thread context through your call stack. Context is plumbing. Run it through every long-lived call site.
Production-ready pattern
Real services need timeouts, proper error wrapping, and safe response handling. Here is a function that fits into a larger architecture.
package main
import (
"context"
"errors"
"fmt"
"log"
"time"
"github.com/sashabaranov/go-openai"
)
// SummarizeText sends a prompt to OpenAI and returns the generated text.
func SummarizeText(ctx context.Context, client *openai.Client, text string) (string, error) {
// Attach a deadline so the request does not hang indefinitely
ctx, cancel := context.WithTimeout(ctx, 15*time.Second)
defer cancel()
// Construct the request with system and user messages
req := openai.ChatCompletionRequest{
Model: openai.GPT4,
Messages: []openai.ChatCompletionMessage{
{Role: openai.ChatMessageRoleSystem, Content: "You are a concise summarizer."},
{Role: openai.ChatMessageRoleUser, Content: text},
},
}
// Send the request and capture the response or error
resp, err := client.CreateChatCompletion(ctx, req)
if err != nil {
return "", fmt.Errorf("openai request failed: %w", err)
}
// Validate that the API actually returned a choice
if len(resp.Choices) == 0 {
return "", errors.New("openai returned an empty response")
}
// Return the generated text
return resp.Choices[0].Message.Content, nil
}
The function accepts an existing context and client, following the "accept interfaces, return structs" pattern where applicable. It creates a child context with a 15-second timeout. The defer cancel() ensures resources are released even if the function returns early. The request includes a system message to guide the model's behavior. Error wrapping with %w preserves the original error chain for debugging. The length check on resp.Choices prevents a runtime panic if the API returns a successful HTTP 200 but an empty choices array.
Go functions that take a context should respect cancellation and deadlines. The receiver name is usually one or two letters matching the type, like (c *Client), but here we pass the client as a regular argument because this is a standalone utility function. Public names start with a capital letter. Private start lowercase. No keywords like public or private. The compiler enforces visibility through capitalization alone.
Pitfalls and runtime behavior
The most common mistake is ignoring the context parameter. Passing context.Background() everywhere works, but it means your request will run until the server times it out or the network drops. Always thread a cancellable context through your call stack. Another trap is assuming resp.Choices[0] always exists. The API can return a 200 status with zero choices during rate limiting or internal errors. Accessing an empty slice panics at runtime. The compiler will not catch this because slice bounds checking happens later.
If you forget to import the package, the compiler rejects the program with undefined: openai. If you pass a string where the library expects a typed constant like openai.GPT4, you get cannot use "gpt-4" (untyped string constant) as openai.ChatModel value in struct literal. These errors save you from silent failures. Network timeouts manifest as context deadline exceeded errors. Rate limits return HTTP 429 with a retry-after header. The library surfaces these as standard Go errors, so you can check them with errors.Is or inspect the HTTP status if the error type implements it.
Goroutine leaks happen when you spawn a background worker to poll the API and forget to cancel its context. Always pair context.WithCancel or context.WithTimeout with a defer cancel(). The worst goroutine bug is the one that never logs. Add structured logging to your error paths so you can trace failed requests in production.
Streaming responses introduce a different failure mode. When you use CreateChatCompletionStream, the library returns a channel that yields tokens as they arrive. If you stop reading from the channel before the server closes it, the underlying HTTP connection leaks. Always range over the stream channel or close it explicitly when you are done. The library does not auto-close streams because it cannot predict when your application logic decides to abort.
When to use this versus alternatives
Use the go-openai library when you want type safety, automatic JSON serialization, and a stable API surface for chat completions and embeddings. Use raw net/http when you need to call a proprietary endpoint that the library does not support yet, or when you want to minimize dependencies for a tiny CLI tool. Use streaming responses when you are building a chat interface and need to display tokens as they arrive instead of waiting for the full completion. Use batch processing when you have thousands of prompts and can tolerate delayed results for lower costs. Use plain sequential code when you don't need concurrency: the simplest thing that works is usually the right thing.