#Build Your First AI Agent in Go with the ADK
Google’s Agent Development Kit (ADK) just got a Go version, and in this tutorial we’re going to use it to build your first AI agent from scratch. It’s a surprisingly small amount of code: a Gemini model doing the thinking, an agent wrapping it with an instruction, a session to hold the conversation, and a runner to drive everything. By the end you’ll have a working agent that answers a question in your terminal — and you’ll understand each of the four pieces well enough to start bending them to your own purposes.
The complete, runnable code for this tutorial lives in the go-ai-examples repo on GitHub — clone it, set your API key, and go run . if you’d rather follow along with the finished project in front of you.
Prerequisites
Before we get started, you’ll need:
- Go 1.23 or newer — the streaming loop uses range-over-function iterators, which landed in Go 1.23.
- A Google AI Studio API key — grab a free one from aistudio.google.com/apikey.
What Is an AI Agent, Really?
There’s no magic here, and it helps to be clear about that up front. An agent is a general-purpose model doing the thinking, wrapped with an instruction that points it at your particular job. You send it a message, it thinks for a moment, and it streams a reply back. That little loop — message in, reasoning, reply out — is the whole idea. Everything else is detail layered on top.
That framing matters because it tells you what an agent is good for. An agent is really just a model you’ve aimed at a task:
- Point it at your documentation and you’ve got a support bot.
- Hand it your team’s style guide and it becomes a code reviewer.
- Give it a search tool and it turns into a research assistant.
It’s the same handful of lines each time — the instruction is what changes everything. We’re going to build the simplest version, and every one of those is just a variation on it.
If you’d rather build an agent loop from scratch to see all the moving parts yourself, I’ve covered that in Building AI Agents in Go. This tutorial takes the other path: letting the ADK handle the plumbing.
Why Use the ADK Instead of Calling the Model Directly?
A fair question — you could call the Gemini API yourself with a plain HTTP request. The reason to reach for the ADK is that it quietly handles the parts that are tedious to get right: the agent loop, sessions and memory, tool calling, and even coordinating multiple agents — all native Go. You bring the idea; it brings the plumbing. As your agent grows from “answers a question” to “calls my APIs and hands work off to other agents,” that plumbing is exactly what you don’t want to be re-inventing.
Setting Up the Project
Let’s make a folder to work in and initialise a Go module so the tooling knows what we’re building:
mkdir adk-agent && cd adk-agent
go mod init github.com/you/adk-agent
Nothing clever yet — just a clean starting point. Swap github.com/you/adk-agent for whatever module path suits you.
Installing the ADK
Next we pull in the ADK itself. A single go get brings down the models, the sessions, and the runner — everything we need to import:
go get google.golang.org/adk
We’ll also use Google’s genai package for constructing messages; go get will resolve it as a dependency of the ADK, but you can pull it in explicitly with go get google.golang.org/genai if your editor complains before the first build.
Setting Your Gemini API Key
There’s just one bit of config. Gemini needs an API key, and the model picks it up from the environment:
export GOOGLE_API_KEY="your-key-here"
Because it’s a secret, keep it in your environment (or a secrets manager) rather than hardcoding it into the source — that way it never ends up committed to git. We’ll read it with os.Getenv in a moment.
Now create a main.go and start with the imports we’ll need:
package main
import (
"context"
"fmt"
"log"
"os"
"google.golang.org/genai"
"google.golang.org/adk/agent"
"google.golang.org/adk/agent/llmagent"
"google.golang.org/adk/model/gemini"
"google.golang.org/adk/runner"
"google.golang.org/adk/session"
)
Each ADK sub-package maps to one of the four pieces we’re about to build — model/gemini, agent/llmagent, session, and runner — so the import block doubles as a preview of the architecture.
Creating the Model
Every agent needs a brain, and ours is going to be Gemini. We create the model with its name and our API key:
ctx := context.Background()
model, err := gemini.NewModel(ctx, "gemini-2.5-flash", &genai.ClientConfig{
APIKey: os.Getenv("GOOGLE_API_KEY"),
})
if err != nil {
log.Fatal(err)
}
From here on, that model is the thing doing all the heavy lifting; everything else just points at it. We’re on gemini-2.5-flash, which is fast and cheap — exactly what you want while you’re still experimenting. Swap in a heavier model later if you need more reasoning power, and the rest of the code doesn’t change.
Defining the Agent
Now for the agent itself, and honestly it’s almost too simple. llmagent.New takes a name, the model, a description, and an instruction:
assistant, err := llmagent.New(llmagent.Config{
Name: "assistant",
Model: model,
Description: "A helpful Go programming assistant.",
Instruction: "You are a concise, friendly assistant for Go developers.",
})
if err != nil {
log.Fatal(err)
}
The Name and Description aren’t just labels. In a larger system, they’re how other agents discover and route to this one — so it’s worth describing what the agent does, not just what it is. But the Instruction is the heart of it: it’s where you tell the agent who it is, how to answer, and what to steer clear of. We’ll come back to that line, because changing it is the most powerful thing you can do here.
Giving the Agent a Session
Before it can hold a conversation, the agent needs a session — somewhere to remember what’s been said:
sessions := session.InMemoryService()
created, err := sessions.Create(ctx, &session.CreateRequest{
AppName: "app",
UserID: "user",
})
if err != nil {
log.Fatal(err)
}
We’re using a simple in-memory session to start with, which is perfect for a script like this. The moment you need conversations to survive a restart — say, a real chat app — you swap InMemoryService() for a persistent implementation (a database-backed one, for example) and the rest of your code stays the same.
Wiring It Together with the Runner
The runner is the piece that ties everything together. You give it the agent and the session, and it drives the actual back-and-forth with the model:
r, err := runner.New(runner.Config{
AppName: "app",
Agent: assistant,
SessionService: sessions,
})
if err != nil {
log.Fatal(err)
}
Think of the runner as the engine room. It’s also where tool calls and multi-agent routing plug in later, so even though we’re only doing a single exchange today, this is the component that grows with you. Note that the AppName matches the one we used when creating the session — that’s how the runner knows which conversations belong to this app.
Sending a Message and Streaming the Reply
Now the fun part. We build a message, hand it to the runner, and loop over the events it streams back, printing each piece of the reply as it arrives:
msg := genai.NewContentFromText("What's a goroutine?", genai.RoleUser)
for event, err := range r.Run(ctx, "user", created.Session.ID(), msg, agent.RunConfig{}) {
if err != nil {
log.Fatal(err)
}
if event.Content != nil {
for _, part := range event.Content.Parts {
fmt.Print(part.Text)
}
}
}
fmt.Println()
The key thing to notice is that r.Run returns an iterator you range over — it streams. You get the answer in chunks as it’s generated, not in one lump at the end, which is exactly what lets you render a response live, token by token, the way a chat UI does. We check each event for an error, then walk its Content.Parts and print the text. The trailing fmt.Println() just adds a newline once the stream finishes.
This is also why you need Go 1.23 or newer: for ... range r.Run(...) is a range-over-function loop, where Run hands each event to the loop body as it produces it.
Running the Agent
With the API key still set in your environment, run it:
go run .
And there it is — a real, working AI agent answering you right there in the terminal:
A goroutine is a lightweight thread managed by the Go runtime. Start one by
putting the "go" keyword in front of a function call, and the scheduler
handles the rest.
No framework spaghetti, no twelve-step setup — built from almost nothing. (The exact wording will vary each run; that’s the model talking, not a fixed string.)
Changing the Agent’s Personality with One Line
Here’s the genuinely fun part, and the thing worth remembering. That Instruction string is your steering wheel. Leave the model, the session, and the runner exactly as they are, and change only this:
Instruction: "You are Chuck, a grumpy senior Go dev. Answer in one sentence. Begrudgingly.",
Run it again and the same question comes back in a completely different voice:
Fine. A goroutine is a function that runs concurrently — you slap "go" in
front of the call. Can I get back to work now?
Same model underneath, same code around it — completely different agent. That’s the whole game: the instruction defines the behaviour. Make it a pirate, a ruthless code reviewer, a patient tutor — and the personality shifts with one line. Go and break it; see what you get.
The Full Program
Here’s everything together, the complete main.go:
package main
import (
"context"
"fmt"
"log"
"os"
"google.golang.org/genai"
"google.golang.org/adk/agent"
"google.golang.org/adk/agent/llmagent"
"google.golang.org/adk/model/gemini"
"google.golang.org/adk/runner"
"google.golang.org/adk/session"
)
func main() {
ctx := context.Background()
// The model — the brain the agent reasons with.
model, err := gemini.NewModel(ctx, "gemini-2.5-flash", &genai.ClientConfig{
APIKey: os.Getenv("GOOGLE_API_KEY"),
})
if err != nil {
log.Fatal(err)
}
// The agent — a name, what it's for, and the instruction that shapes it.
assistant, err := llmagent.New(llmagent.Config{
Name: "assistant",
Model: model,
Description: "A helpful Go programming assistant.",
Instruction: "You are a concise, friendly assistant for Go developers.",
})
if err != nil {
log.Fatal(err)
}
// A session holds the conversation; the runner drives the agent.
sessions := session.InMemoryService()
created, err := sessions.Create(ctx, &session.CreateRequest{
AppName: "app",
UserID: "user",
})
if err != nil {
log.Fatal(err)
}
r, err := runner.New(runner.Config{
AppName: "app",
Agent: assistant,
SessionService: sessions,
})
if err != nil {
log.Fatal(err)
}
// Send a message and stream the reply.
msg := genai.NewContentFromText("What's a goroutine?", genai.RoleUser)
for event, err := range r.Run(ctx, "user", created.Session.ID(), msg, agent.RunConfig{}) {
if err != nil {
log.Fatal(err)
}
if event.Content != nil {
for _, part := range event.Content.Parts {
fmt.Print(part.Text)
}
}
}
fmt.Println()
}
Four small pieces — model, agent, session, runner — and one clean flow between them.
Frequently Asked Questions
What is the Agent Development Kit (ADK) for Go?
The ADK is Google’s open-source framework for building AI agents, and google.golang.org/adk is its native Go implementation. It gives you composable building blocks — models, agents, sessions, runners, tools, and multi-agent orchestration — so you don’t have to hand-roll the agent loop or wire up the Gemini API yourself. You can find it on GitHub at github.com/google/adk-go.
How do I set up my Gemini API key for the ADK?
Get a key from Google AI Studio, then export it as the GOOGLE_API_KEY environment variable: export GOOGLE_API_KEY="your-key-here". The gemini.NewModel call reads it via os.Getenv("GOOGLE_API_KEY"). Keep the key in your environment or a secrets manager rather than hardcoding it, so it never gets committed to source control.
Why does the ADK streaming loop need Go 1.23?
The runner’s Run method returns a range-over-function iterator — for event, err := range r.Run(...). That syntax, where you range directly over a function, was introduced in Go 1.23. On an older Go version the code won’t compile, so make sure your toolchain is 1.23 or newer.
How do I change what my agent does?
Change the Instruction field on the llmagent.Config. The instruction is the natural-language prompt that defines the agent’s role, tone, and boundaries, and it’s the single most influential setting. The model, session, and runner can stay identical while the instruction turns the same agent into a support bot, a code reviewer, or a grumpy senior dev.
What’s the difference between a session and a runner?
The session is where the conversation lives — it remembers what’s been said. The runner is the engine that drives each exchange: you hand it the agent and the session, and it manages the back-and-forth with the model (and, later, tool calls and multi-agent routing). You can have a session without running anything, but the runner needs a session to know where to read and write the conversation.
What’s Next
You’ve got the foundation. Both of the big next steps build directly on the code you’ve just written:
- Tools — let your agent call your own Go functions and hit APIs, so it does real work instead of just talking. A great way to expose tools to agents is the Model Context Protocol; see Building an MCP Server in Go.
- Multi-agent systems — have agents hand tasks off to one another for bigger jobs, using the
NameandDescriptionfields we set so they can discover and route to each other. - For more on the bigger picture of what agents are and how to reason about them, read Building AI Agents in Go.
Continue Learning
Building AI Agents in Go
Learn how to build AI agents in Go that can use tools, make decisions, and complete tasks autonomously.
Building an MCP Server in Go
Build a Model Context Protocol (MCP) server in Go with mark3labs/mcp-go — define a tool, handle tool calls, and serve it over stdio to clients like Claude.
Getting Started with the Claude API in Go
Learn how to use the Anthropic Claude API in Go — set up the SDK, send your first message, and build a multi-turn conversation that maintains context.
Building AI Applications with LangChainGo
Learn how to build AI-powered applications in Go using the LangChainGo library with practical examples.