The discourse around Agentic AI is a mix of revolutionary hype and deep skepticism. While some developers see a future of unparalleled productivity, others see tools that fail on complex tasks and generate more work than they save. The key to bridging this gap lies not in the agent alone, but in creating an environment where it can succeed. But what does an ‘agent-friendly’ codebase actually look like?
The answer, as Armin Ronacher compellingly argues in his post [1], is that the principles that make code easy for an agent to understand often make it better for humans, too. This isn’t about exotic new patterns; it’s about leaning into languages and ecosystems that prioritize simplicity, explicitness, and stability.
This is where Go excels. Let’s walk through building a simple web service to demonstrate five key Go features that create a predictable, reliable, and testable environment where an AI agent can thrive.
Our goal: Build a web service that greets a user, logging a user_id for internal auditing while ensuring this PII is never exposed in the public response.
1. Context System: Propagating Data Implicitly
As Armin Ronacher points out [1]:
Context system: Go provides a capable copy-on-write data bag that explicitly flows through the code execution path, similar to contextvars in Python or .NET’s execution context. Its explicit nature greatly simplifies things for AI agents. If the agent needs to pass stuff to any call site, it knows how to do it.
Rationale: We need to pass a request_id and a sensitive user_id through our application for logging and auditing. Instead of cluttering every function signature with these parameters, we’ll use Go’s context package. It acts as a request-scoped ‘data bag,’ making data available where needed without being part of the core function signature. This is a clear, explicit pattern that’s easy to follow.
main.go:
package main
import (
"context"
"fmt"
"log"
)
// Use unexported custom types for context keys to prevent collisions.
type contextKey string
const (
requestIDKey = contextKey("request_id")
userIDKey = contextKey("user_id") // This is our PII.
)
// A helper simulating a secure audit log. It knows how to extract PII from the context.
func logAuditEvent(ctx context.Context, event string) {
requestID, _ := ctx.Value(requestIDKey).(string)
userID, _ := ctx.Value(userIDKey).(string)
log.Printf("[AUDIT] Req: %s, User: %s, Event: %s", requestID, userID, event)
}
// The core logic is clean. It only takes `name` as a parameter.
func Greet(ctx context.Context, name string) string {
logAuditEvent(ctx, "User greeted")
// The public-facing return value is guaranteed to be clean.
return fmt.Sprintf("Hello, %s!", name)
}
func main() {
// Create a context and add our values. This is the "copy-on-write" part.
ctx := context.WithValue(context.Background(), requestIDKey, "xyz-123")
ctx = context.WithValue(ctx, userIDKey, "usr_secret_pii_alice")
greeting := Greet(ctx, "Alice")
fmt.Println(greeting)
}
Run it:
$ go run main.go
[AUDIT] Req: xyz-123, User: usr_secret_pii_alice, Event: User greeted
Hello, Alice!
The user_id is logged but not exposed, just as planned.
Note: While the Go community advises against using context.WithValue for core application data to avoid opaque dependencies, this pattern is exceptionally powerful for securing sensitive, request-scoped data like PII. By placing the user_id in the context instead of passing it as a direct function parameter, we drastically reduce its visibility. The core Greet function remains clean and unaware of the PII’s existence. Only specific, trusted functions (like our logAuditEvent) are designed to extract and use this sensitive data. This creates a strong separation of concerns, making it much harder for a developer—or an AI agent—to accidentally leak the user_id into a public-facing response or a general log message. For an agent, this provides a clear and secure convention: business logic functions operate on their explicit inputs, while PII is handled through a distinct and isolated mechanism.
2. Test Caching: Achieving a Fast Feedback Loop
As Armin Ronacher notes [1]:
Test caching: Surprisingly crucial for efficient agentic loops. In Rust, agents sometimes fail because they misunderstand cargo test’s invocation syntax. In Go, tests run straightforwardly and incrementally, significantly enhancing the agentic workflow. It does not need to figure out which tests to run, go does.
Rationale: In an agentic workflow, tests are run constantly. We need a fast, efficient feedback loop. Go’s built-in test runner automatically caches test results for packages that haven’t changed, making the command go test incredibly fast on subsequent runs. We don’t need to manually select which tests to run; the toolchain handles it.
main_test.go:
package main
import (
"context"
"testing"
)
func TestGreet(t *testing.T) {
ctx := context.WithValue(context.Background(), userIDKey, "test_pii_value")
result := Greet(ctx, "Bob")
expected := "Hello, Bob!"
// We test the public output to ensure no PII is leaking.
if result != expected {
t.Errorf("Greet() returned %q; want %q", result, expected)
}
}
Run the test:
$ go test
PASS
ok myproject 0.002s
$ go test # Run it again immediately
ok myproject (cached)
The cached result is nearly instantaneous, providing the rapid feedback essential for an efficient development loop.
3. ‘Sloppy’ Go: Simple and Predictable Error Handling
From [1]:
Go is sloppy: Rob Pike famously described Go as suitable for developers who aren’t equipped to handle a complex language. Substitute ‘developers’ with ‘agents,’ and it perfectly captures why Go’s simplicity benefits agentic coding.
Rationale: The language should make it easy to handle failures. Go’s if err != nil pattern, while sometimes verbose, is extremely simple and predictable. There are no complex exception hierarchies to learn. This simplicity is a feature, making the code easy to generate and reason about. Let’s add a rule: the name ‘Admin’ is forbidden.
main.go (modified Greet function):
// ... (imports and other functions remain) ...
import "errors"
func Greet(ctx context.Context, name string) (string, error) { // Note the new error return value
if name == "Admin" {
logAuditEvent(ctx, "Attempted to greet forbidden name 'Admin'")
// A simple, standard library error is often all you need.
return "", errors.New("cannot greet Admin")
}
logAuditEvent(ctx, "User greeted")
return fmt.Sprintf("Hello, %s!", name), nil
}
main_test.go (updated):
package main
import (
"context"
"testing"
)
func TestGreet_Success(t *testing.T) {
// ... (same as the previous test)
}
func TestGreet_ForbiddenName(t *testing.T) {
ctx := context.Background()
_, err := Greet(ctx, "Admin")
if err == nil {
t.Fatal("Expected an error for name 'Admin', but got none")
}
}
This pattern is trivial to generate and universally understood in the Go ecosystem.
4. Structural Interfaces: Polymorphism Without Ceremony
As Armin Ronacher explains [1]:
Structural interfaces: interfaces in Go are structural. If a type has the methods an interface expects, then it conforms. This is incredibly easy for LLMs to ‘understand’. There is very little surprise for the agent.
Rationale: We need a new type of greeter that doesn’t log PII. In many languages, this would require finding the interface definition and explicitly declaring class AnonymousGreeter implements Greeter. Go’s structural interfaces are simpler. A type satisfies an interface automatically if it has the required methods. This is ‘duck typing,’ but checked at compile time.
main.go (refactored):
package main
// ... (imports and context keys) ...
// 1. Define the "shape" of a Greeter.
type Greeter interface {
Greet(ctx context.Context, name string) (string, error)
}
// 2. Wrap our existing logic in a struct. It already matches the Greeter shape.
type PIIGreeter struct{}
func (g PIIGreeter) Greet(ctx context.Context, name string) (string, error) { /* ... old Greet logic ... */ }
// 3. Create a new greeter. It has no knowledge of the Greeter interface.
type AnonymousGreeter struct{}
// Because it has a matching Greet method, it IS a Greeter.
func (g AnonymousGreeter) Greet(ctx context.Context, name string) (string, error) {
if name == "Admin" { return "", errors.New("cannot greet Admin") }
log.Printf("[AUDIT] An anonymous user was greeted") // No PII is accessed or logged.
return fmt.Sprintf("Hello, %s!", name), nil
}
func main() {
var greeter Greeter
greeter = PIIGreeter{} // This works.
greeter = AnonymousGreeter{} // This also works.
// ...
}
This is powerful. To make a new type conform, you just give it the right methods. There is no extra boilerplate or ceremony.
5. A Stable Ecosystem: Building a Durable Web Service
From [1]:
Go has low eco-system churn: Go’s entire ecosystem embraces backwards compatiblity and explicit version moves. This greatly reduces the likelihood of AI generating outdated code — starkly contrasting JavaScript’s fast-moving ecosystem for instance.
Rationale: To turn this into a web service, we need an HTTP library. Unlike ecosystems with many competing web frameworks, Go has a powerful and incredibly stable standard library (net/http) that is the default choice for most services. Its API has remained consistent for years, meaning that code examples and LLM training data are rarely outdated.
main.go (final version):
package main
import (
"context"
"errors"
"fmt"
"log"
"net/http" // The stable, built-in choice.
)
// Use unexported custom types for context keys to prevent collisions.
type contextKey string
const (
requestIDKey = contextKey("request_id")
userIDKey = contextKey("user_id") // This is our PII.
)
// Greeter is the interface that defines the greeting behavior.
type Greeter interface {
Greet(ctx context.Context, name string) (string, error)
}
// PIIGreeter is a concrete implementation of Greeter that handles PII.
type PIIGreeter struct{}
// logAuditEvent is a helper function that securely logs PII from the context.
// Only this function has the knowledge to extract and handle the userIDKey.
func logAuditEvent(ctx context.Context, event string) {
requestID, _ := ctx.Value(requestIDKey).(string)
userID, _ := ctx.Value(userIDKey).(string)
log.Printf("[AUDIT] Req: %s, User: %s, Event: %s", requestID, userID, event)
}
// Greet implements the Greeter interface for PIIGreeter.
// Note that this core business logic does not directly handle PII.
// It relies on the context and helper functions for secure auditing.
func (g PIIGreeter) Greet(ctx context.Context, name string) (string, error) {
if name == "Admin" {
logAuditEvent(ctx, "Attempted to greet forbidden name 'Admin'")
return "", errors.New("cannot greet Admin")
}
logAuditEvent(ctx, "User greeted")
return fmt.Sprintf("Hello, %s!", name), nil
}
// greeterHandler is an HTTP handler that uses our Greeter interface.
func greeterHandler(greeter Greeter) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
// In a real app, request_id and user_id would come from middleware
// after authentication and request tracing is set up.
requestContext := context.WithValue(r.Context(), requestIDKey, "web-req-98765")
piiContext := context.WithValue(requestContext, userIDKey, "web_user_pii_112233")
greeting, err := greeter.Greet(piiContext, name)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
fmt.Fprintln(w, greeting)
}
}
func main() {
greeter := PIIGreeter{}
http.HandleFunc("/greet", greeterHandler(greeter))
log.Println("Starting server on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
Run the service:
$ go run main.go
Starting server on :8080
# In another terminal:
$ curl "http://localhost:8080/greet?name=WebAppUser"
Hello, WebAppUser!
The server’s log will show the audit trail with the user_id, while the user only sees the clean output.
Conclusion: Durable Principles for an Agentic World
The five features we’ve explored—a clear context system, fast test caching, simple error handling, structural interfaces, and a stable ecosystem—are not just convenient quirks of Go. They represent a philosophy of software design that is remarkably well-suited for the agentic era.
These are the architectural pillars of a predictable and robust system:
- Explicit Data Flow with
context. - Rapid Feedback with
go test. - Predictable Control Flow with
if err != nil. - Decoupled Logic with structural interfaces.
- Long-Term Stability with the standard library.
Writing simple, explicit, and well-tested code has always been best practice. The rise of Agentic AI doesn’t change these principles; it amplifies their importance and provides immediate, measurable feedback on how well we adhere to them. By optimizing for clarity, we are not just making our codebases friendlier for AI agents—we are making them more robust, maintainable, and ultimately, better for the humans who build and depend on them.
References
I hope you found this article helpful. If you want to take your agentic AI to the next level, consider booking a consultation or subscribing to premium content.