Skip to main content

Golang

2026

LLM and Go: Retrieval-Augmented Generation (RAG) with OpenAI - Part 2

·2335 words·11 mins· loading · loading
In the previous article we built a feeder application: it reads fairy tales from a Markdown file, generates an OpenAI embedding for each one, and stores the text, the vector, and a small metadata blob in Postgres through pgvector. That gave us a table full of vectors, but no way to query it from a running application — the data sat there, searchable only by hand, through a database client. This article closes that gap. We build a small conversational agent that searches the fairy tale library through a tool and answers using only what that tool returns, not whatever GPT happens to remember about fairy tales from its training data. The model decides on its own when a search is needed, based on what the user actually asks. Setting Up The Project # Everything here builds on top of the previous article, so the same Docker Compose setup (a Postgres 17 container with the pgvector extension baked in) needs to be running, with the fairy_tales table already populated by the feeder. If that step has not happened yet, the first article walks through the Docker Compose file, the table schema, and the feeder application that fills it with fifty fairy tales. Assuming the feeder already ran, the table looks like this: Fairy tales table in pgvector The data is in place, but the goal for this article is different from a one-off SQL query: we want an AI agent to decide, on its own, when a search is needed and what to search for. Running a hardcoded query against every user message would not give the model that choice. That means giving the agent a tool — a function with a name, a description, and a parameter schema — that it can choose to call. The tool itself is allowed to call the OpenAI API. The agent calls GPT to decide it needs information, and the tool it invokes then calls the Embeddings API on its own to turn that request into a vector before it ever touches Postgres. Nothing forces every OpenAI call in an application through the same layer: a tool is just a Go function, and that function can talk to whatever it needs to get its job done.

LLM and Go: Retrieval-Augmented Generation (RAG) with OpenAI - Part 1

·2265 words·11 mins· loading · loading
In the previous article we gave Gemini access to tools and let it call them, closing the gap between an LLM and the outside world. This article closes a different gap: getting an LLM to answer questions about data it has never seen. Ask GPT-4o about a document that was never part of its training data, and it has two options: admit that it does not know, or guess convincingly. Neither answer is acceptable in an application we build and ship. Retrieval-Augmented Generation, RAG, solves this without retraining or fine-tuning anything: we store our own data as vectors, look up the entries closest to a question, and hand only those entries to the LLM as context. This article is the first of two parts. Here we build the feeder application: a small Go program that reads a set of fairy tales from a Markdown file, generates embeddings for each one using the OpenAI API, and stores them in Postgres with the pgvector extension. The second part covers the retrieval side, querying that data and feeding it back into a chat completion. How RAG Works # Retrieval-Augmented Generation is a pattern that pairs a search step with a generation step: we retrieve relevant text from our own storage, then ask an LLM to generate an answer using that text as context. What this means in practice is that the model’s knowledge stops being the only source of truth — our database becomes one too. To make retrieval work, we first need a way to compare two pieces of text for similarity. That is what an embedding provides: a fixed-size array of floating-point numbers (1536 of them for OpenAI’s text-embedding-3-small model) that represents the meaning of a piece of text as a point in a high-dimensional space. Texts with similar meaning end up close to each other in that space; texts about unrelated topics end up far apart. Visualizing 1536 dimensions is not something anyone can do directly, but the same idea holds in three dimensions: each text becomes a point in space, and the closer two points sit, the more similar their meanings. Text as vectors in embedding space Postgres and pgvector give us three ways to measure that distance: Euclidean distance (<->), cosine distance (<=>), and inner product (<#>). Cosine distance is the most common choice for text embeddings, because it measures the angle between two vectors rather than their magnitude, which matters more for meaning than for raw scale.

LLM and Go: Gemini integration via Interactions API

·2880 words·14 mins· loading · loading
The previous article in this series covered the Generate Content API — how to set up a client, let the Chats helper manage conversation history, and call external tools. This article covers the other Gemini interface: the Interactions API. The Interactions API moves conversation state from the client to Google’s servers. You no longer maintain a chat object and let a helper accumulate history locally; instead, you track an interaction ID and pass it back on the next request. That is a meaningful shift for agent-oriented applications — less client-side bookkeeping, but also less transparency into exactly what the server reconstructs on each turn. Understanding the trade-offs between the two APIs is worth doing before choosing which one to build on. Interactions API # Google made the Interactions API generally available recently, positioning it as the unified way to call both plain Gemini models and its specialised agents through a single endpoint. The Generate Content API is stateless by default — every request must carry the full conversation history, whether you build that history yourself or let the Chats helper do it for you locally. The Interactions API inverts this: conversation state lives on Google’s servers, and you reference previous turns by ID rather than resending them. Both APIs give you access to the same underlying models and tool-calling mechanics. The difference is where the orchestration responsibility sits. The table below, first introduced in the Generate Content API article, summarizes the trade-offs: Feature Generate Content API Interactions API Conversation state Client-managed, or via the Chats helper Server-managed via previous_interaction_id History management Manual — resent with every request, unless using Chats Automatic, referenced by ID Tool support Manual function calling Unified tool and agent invocation Background execution No Yes Data retention None — nothing stored server-side 55 days (paid), 1 day (free), unless store=false Control Full Reduced Best for Custom agents, full control, simplicity Long-running interactions, agent orchestration The Generate Content API is the right default when you want to control exactly what the model sees and keep the request/response model explicit. The Interactions API reduces boilerplate and fits well when background execution or server-managed state actually matter for what you are building. In this article we build the same conversational agent we built before — but with the Interactions API driving state management.

LLM and Go: Gemini Integration via Generate Content API

·2874 words·14 mins· loading · loading
This series has so far covered two providers: OpenAI, across Chat Completions and Responses, and Anthropic, across the Messages API and its output parameters. Gemini is the third provider worth knowing in Go, and it comes with its own set of design decisions — a different SDK, a different way of managing conversation history, and, as of recently, two separate APIs doing largely the same job. This article builds the same conversational agent from earlier in the series, this time on top of Google’s Gemini API, using the genai Go SDK. By the end, you will have a working agent that maintains conversation history and can call external tools to answer questions it otherwise could not — and a clear picture of which of Gemini’s two APIs to reach for. A short introduction to Gemini # The path to large language models runs through a decade of incremental progress in deep learning. Early models like word2vec and GloVe learned to embed words into dense vector spaces, capturing semantic relationships between terms. The transformer architecture, introduced by Google in 2017, changed the trajectory of the field — it processes sequences in parallel using attention mechanisms that capture long-range dependencies far more effectively than recurrent networks. This architectural shift made it practical to train models on orders of magnitude more data. GPT-1 in 2018 showed that large-scale unsupervised pre-training followed by fine-tuning could match or beat purpose-built models across a range of language tasks. Understanding what these models actually do removes a lot of the mysticism around them. An LLM is, at its core, a next-token predictor. It takes a sequence of tokens as input and outputs a probability distribution over the vocabulary for the next token. The transformer’s attention mechanism allows every token in the input to attend to every other token, building a rich contextual representation before making that prediction. Training adjusts billions of parameters to minimise prediction error across enormous text corpora. What emerges is a model with broad world knowledge encoded in its weights — not because it was taught facts directly, but because predicting text well requires internalising the structure of the world that produced that text. Gemini is Google DeepMind’s model family, introduced in late 2023 as the first model built jointly by Google Brain and DeepMind after the two research groups merged. Unlike earlier generations of models that had multimodal capabilities bolted on top of a text-only foundation, Gemini was trained natively across text, images, audio, and video from the start, which is part of why it handles mixed-input tasks and tool use without the awkward seams you sometimes see in models where multimodality was added later. For developers, Google exposes Gemini through the Gemini API — and, as of recently, through two distinct interfaces for that API, which is the subject of the next section.

LLM and Go: Investigating Anthropic Messages API

·2276 words·11 mins· loading · loading
In the previous article I covered the fundamentals of Anthropic’s Messages API: setting up a client, maintaining conversation history, and integrating tools. That was enough to build a working conversational agent. This article goes a level deeper — into the API parameters that shape what the model returns and how it thinks. Two parameters stand out as particularly useful in production: output_config.format and output_config.effort. The first gives you control over the structure of the model’s output. The second controls how much the model reasons before responding — which turns out to matter more than you might expect once you start caring about latency and cost. Messages API details # The Messages API endpoint accepts a rich set of parameters. Most have sensible defaults and you will rarely touch them, but understanding what is available saves you from reaching for workarounds that already exist in the API. The table below covers a selection of the current parameters from the API reference: Parameter Type Description model string ID of the model to use messages array Conversation history as an ordered list of messages system string/array System prompt that sets the model’s behaviour, kept separate from messages max_tokens integer Maximum tokens the model may generate — required on every request output_config.format object Constrains the response to a JSON Schema output_config.effort string Reasoning depth: low, medium, high, xhigh, max thinking object Enables and configures extended or adaptive thinking temperature number Sampling temperature from 0 to 1; higher values produce more random output top_p number Alternative to temperature; nucleus sampling probability mass top_k integer Restricts sampling to the top K most likely tokens stop_sequences array Custom sequences at which the API stops generating stream boolean Stream partial responses as server-sent events tools array List of tools the model may call tool_choice object Controls which tool the model calls metadata object Arbitrary metadata about the request, such as an end-user ID In this article we focus on output_config.format and output_config.effort — two parameters with a direct, visible impact on production systems. Information extraction with output_config.format # The format field inside output_config controls how the model structures its output. By default, Claude replies with plain text. Setting output_config.format to a json_schema document constrains the response to conform to that schema — Anthropic calls this structured outputs.

LLM and Go: Anthropic Integration via Messages API

·2757 words·13 mins· loading · loading
The previous two articles in this series covered OpenAI’s side of this problem: Chat Completions, where the client owns the entire conversation history, and Responses, where OpenAI’s servers do. Anthropic’s Claude models are built on a third set of API decisions — close enough to Chat Completions in shape that the same agent design carries over almost directly, but different enough in the details, like how the system prompt is passed and how a response is structured, that it is worth building the same agent again to see exactly where. This article rebuilds that agent on Anthropic’s Messages API — the stateless, request-based interface behind Claude. By the end, you will have a working conversational agent that can call external tools to answer questions it otherwise could not, and a clear picture of what changes when you swap providers. A short introduction to Claude and Anthropic # The path to large language models runs through a decade of incremental progress in deep learning. Early models like word2vec and GloVe learned to embed words into dense vector spaces, capturing semantic relationships between terms. The transformer architecture, introduced by Google in 2017, changed the trajectory of the field — it processes sequences in parallel using attention mechanisms that capture long-range dependencies far more effectively than recurrent networks. This architectural shift made it practical to train models on orders of magnitude more data. GPT-1 in 2018 showed that large-scale unsupervised pre-training followed by fine-tuning could match or beat purpose-built models across a range of language tasks. Understanding what these models actually do removes a lot of the mysticism around them. An LLM is, at its core, a next-token predictor. It takes a sequence of tokens as input and outputs a probability distribution over the vocabulary for the next token. The transformer’s attention mechanism allows every token in the input to attend to every other token, building a rich contextual representation before making that prediction. Training adjusts billions of parameters to minimise prediction error across enormous text corpora. What emerges is a model with broad world knowledge encoded in its weights — not because it was taught facts directly, but because predicting text well requires internalising the structure of the world that produced that text. Claude is Anthropic’s model family, and the company itself was founded in 2021 by a group of former OpenAI researchers, including Dario Amodei and Daniela Amodei, with AI safety as its founding focus. What differentiates Claude’s training pipeline from a pure RLHF approach is Constitutional AI — a technique where the model critiques and revises its own outputs against a written set of principles, rather than relying solely on human raters to judge every response. The goal is a model that behaves predictably even in situations no human rater explicitly labelled. For developers, the way to reach any Claude model programmatically is the Messages API — the stateless, request-based interface used throughout this article.

LLM and Go: OpenAI integration via Responses API

·2365 words·12 mins· loading · loading
The previous two articles in this series covered the Chat Completions API — how to set up a client, maintain conversation history manually, call external tools, and control output structure with response_format. That API gives you full control and a clear mental model of what goes over the wire. This article covers the other primary OpenAI interface: the Responses API. The Responses API moves conversation state from the client to OpenAI’s servers. You no longer maintain a history slice and re-send it with every call. Instead, you track a response ID and pass it back on the next request. That is a meaningful shift for agent-oriented applications — less maintenance, but also less transparency. Understanding the trade-offs between the two APIs is worth doing before choosing which one to build on. Responses API # OpenAI introduced the Responses API in 2025, positioning it as the foundation for building agents. The Chat Completions API is stateless — every request must carry the full conversation history, and the client owns that state entirely. The Responses API inverts this: conversation state lives on OpenAI’s servers, and you reference previous turns by ID rather than re-sending them. Both APIs give you access to the same underlying models and tool-calling mechanics. The difference is where the orchestration responsibility sits. The table below, first introduced in the Chat Completions API article, summarizes the trade-offs: Feature Chat Completions API Responses API Conversation state Client-managed Server-managed History management Manual — sent with every request Automatic Tool support Manual function calling Built-in tools (web search, code interpreter) Streaming Yes Yes Control Full Limited Vendor coupling Low Higher Best for Custom agents, full control Rapid prototyping, built-in tooling The Chat Completions API is the right default when you want to control exactly what the model sees and when you need portability across providers. The Responses API reduces boilerplate and fits well when you want to prototype quickly or lean on OpenAI’s managed tooling. In this article we build the same conversational agent we built before — but with the Responses API driving state management. First AI agent # The openai-go SDK covers both APIs under one package. The same client initialization you used for Chat Completions works here. To call the API, you need a secret key from platform.openai.com — navigate to the API Keys section, generate a key, and store it in an environment variable.

LLM and Go: Investigating OpenAI Chat Completions API

·2132 words·11 mins· loading · loading
In the previous article I covered the fundamentals of the Chat Completions API: setting up a client, maintaining conversation history, and integrating tools. That was enough to build a working conversational agent. This article goes a level deeper — into the API parameters that shape what the model returns and how it thinks. Two parameters stand out as particularly useful in production: response_format and reasoning_effort. The first gives you control over the structure of the model’s output. The second controls how much the model reasons before responding — which turns out to matter more than you might expect once you start caring about latency and cost. Chat Completions API details # The Chat Completions API endpoint accepts a rich set of parameters. Most have sensible defaults and you will rarely touch them, but understanding what is available saves you from reaching for workarounds that already exist in the API. The table below covers the current non-deprecated parameters from the API reference: Parameter Type Description model string ID of the model to use messages array Conversation history as an ordered list of messages response_format object Output format: text, json_object, or json_schema reasoning_effort string Reasoning intensity for reasoning models: low, medium, high temperature number Sampling temperature from 0 to 2; higher values produce more random output top_p number Alternative to temperature; nucleus sampling probability mass max_completion_tokens integer Maximum tokens the model may generate in the response n integer Number of completion choices to return stream boolean Stream partial responses as server-sent events stop string/array Sequences at which the API stops generating presence_penalty number Penalises new tokens based on whether they appear in the text so far frequency_penalty number Penalises new tokens based on their frequency in the text so far tools array List of tools (functions) the model may call tool_choice string/object Controls which tool the model calls seed integer Seed for deterministic sampling user string Unique identifier for the end user In this article we focus on response_format and reasoning_effort — two parameters with a direct, visible impact on production systems. Information extraction with response_format # The response_format parameter controls how the model structures its output. The default is plain text. Setting it to json_object tells the model to return valid JSON, but gives you no control over the schema. Setting it to json_schema goes further: you provide a JSON Schema document and the model guarantees its output will conform to it. OpenAI calls this structured output.

LLM and Go: OpenAI Integration via Chat Completions API

·2721 words·13 mins· loading · loading
For most of my career, integrating external intelligence into an application meant calling a rules engine, training a custom classifier, or encoding business logic that someone had painfully documented in a spreadsheet. The idea that I could describe a task in plain language and have a model respond with genuine reasoning was not something I expected to become production-ready in my working life. Then GPT happened, and it changed what backend developers need to know. This article is the first in a series on using LLMs in Go. We start with the OpenAI Chat Completions API — the stateless, request-based interface that gives you direct control over every aspect of the conversation. By the end, you will have a working conversational agent that can call external tools to answer questions it otherwise could not. A short introduction to ChatGPT and OpenAI # The path to large language models runs through a decade of incremental progress in deep learning. Early models like word2vec and GloVe learned to embed words into dense vector spaces, capturing semantic relationships between terms. The transformer architecture, introduced by Google in 2017, changed the trajectory of the field — it processes sequences in parallel using attention mechanisms that capture long-range dependencies far more effectively than recurrent networks. This architectural shift made it practical to train models on orders of magnitude more data. GPT-1 in 2018 showed that large-scale unsupervised pre-training followed by fine-tuning could match or beat purpose-built models across a range of language tasks. Understanding what these models actually do removes a lot of the mysticism around them. An LLM is, at its core, a next-token predictor. It takes a sequence of tokens as input and outputs a probability distribution over the vocabulary for the next token. The transformer’s attention mechanism allows every token in the input to attend to every other token, building a rich contextual representation before making that prediction. Training adjusts billions of parameters to minimise prediction error across enormous text corpora. What emerges is a model with broad world knowledge encoded in its weights — not because it was taught facts directly, but because predicting text well requires internalising the structure of the world that produced that text. ChatGPT is OpenAI’s conversational product built on the GPT model series. What set it apart from raw GPT-3 was the addition of reinforcement learning from human feedback (RLHF) — a technique that fine-tunes the base model to follow instructions and produce responses that human raters judge as helpful and safe. When ChatGPT launched in late 2022, it became one of the fastest-adopted consumer products in history. For developers, the more relevant artefact is the API behind it — specifically the Chat Completions API, which gives programmatic access to the same models powering the product.

Go Tutorial: Iterators

·2086 words·10 mins· loading · loading
Go 1.22 introduced range over functions, and Go 1.23 brought the iter package to go with it. Together they gave iterators a proper place in the language. Before that, iterating over custom data structures meant either returning slices upfront — loading everything into memory — or writing callback-based helpers that nobody could agree on naming. I have seen both approaches and neither felt right. The core idea behind iterators is straightforward: instead of computing all values upfront and handing them back as a list, you compute each value on demand and yield it to the caller one at a time. The caller controls when to stop. This matters any time you are working with large or potentially infinite sequences. This article walks through why iterators exist in Go, how the yield-based pattern works, what the iter package provides, and where the current limits of the feature sit. Why do we need Iterators? # The simplest case for iteration is a slice of numbers. You range over it, print each value, move on. func main() { numbers := []int{1, 2, 3, 4, 5} for _, i := range numbers { fmt.Println(i) } } // OUT: // 1 // 2 // 3 // 4 // 5 That works fine until the collection gets large. If you need to generate a million numbers, you have to allocate memory for all of them before you can even start ranging. func main() { n := 1_000_000 numbers := make([]int, n) for i := range numbers { numbers[i] = i * 2 } for _, i := range numbers { fmt.Println(i) } } // OUT: // 1 // 2 // ... You can always add a break once you hit your threshold, but the damage is already done — the entire slice was allocated upfront. In other cases, you might not even know how many items you will need. The for range loop can iterate for some time, until it reaches the breakpoint, depending on some value provided in the item. In such cases, the size of such a list must be not just too big, but absolutely unpredictable. func main() { n := 1_000_000 numbers := make([]int, n) for i := range numbers { numbers[i] = i * 2 } for _, i := range numbers { fmt.Println(i) if i > 10 { break } } } // OUT: // 1 // 2 // 4 // 8 // 10 // 12 In a real application, the decision about when to stop often happens dynamically — driven by user input, a timeout, or a condition that evaluates to true before the fifth item. Allocating a million items and then breaking on the fifth is wasteful. This is exactly the problem iterators solve.