How to Use LangChain Alternatives in Go (LangChainGo)
You built a chatbot in Python using LangChain. It works. You paste prompts, chain models, and query vector stores with a few lines of code. Now you need that same logic in a Go service that handles high throughput, or you're building a CLI tool where Python feels too heavy. You search for "LangChain for Go" and find langchaingo. It looks familiar, but the API feels different. The imports are verbose. The error handling is explicit. This is how Go does things.
The abstraction layer in Go
LangChainGo provides a unified interface for Large Language Models, embeddings, and vector stores. Python's LangChain hides the differences between OpenAI, Anthropic, and local models behind a single API. LangChainGo does the same. You write code against langchaingo types, and you can swap the underlying provider by changing the initialization code. This abstraction saves you from rewriting prompt logic when you switch from Gemini to Ollama. It also gives you a consistent way to handle chains, memory, and document loaders.
Go code follows the rule "accept interfaces, return structs." LangChainGo embraces this. Functions take llms.Model interfaces, so you can pass any implementation that satisfies the contract. The googleai package returns a concrete struct, but you treat it as an interface. This keeps your application code decoupled from the provider. You can unit test with a mock model without changing the rest of your logic.
Abstractions save time until they leak. Choose the layer that matches your problem.
Minimal example: call a model
Here's the simplest setup: initialize a client, send a prompt, get a response.
package main
import (
"context"
"fmt"
"log"
"os"
"github.com/tmc/langchaingo/llms"
"github.com/tmc/langchaingo/llms/googleai"
)
func main() {
// Context carries cancellation and deadlines. Always pass it first.
ctx := context.Background()
apiKey := os.Getenv("GEMINI_API_KEY")
// Create the Gemini client with configuration options.
client, err := googleai.New(ctx,
googleai.WithAPIKey(apiKey),
googleai.WithDefaultModel("gemini-pro"),
)
if err != nil {
log.Fatal(err)
}
// Generate content using the standard LLM interface.
result, err := llms.GenerateFromSinglePrompt(ctx, client, "Explain Go interfaces in one sentence.")
if err != nil {
log.Fatal(err)
}
fmt.Println(result)
}
Context is plumbing. Run it through every long-lived call site.
What happens under the hood
The googleai.New function builds a client configured to talk to Google's API. It returns a value that implements the llms.Model interface. Go uses structural typing for interfaces. You don't declare that a type implements an interface. The compiler checks the method set. If the type has all the methods required by the interface, it satisfies the interface. This means googleai.Client works anywhere llms.Model is expected, even though the packages are different.
When you call llms.GenerateFromSinglePrompt, LangChainGo takes your prompt, wraps it in the format the provider expects, sends the HTTP request, parses the response, and returns the text. If the API key is missing or the network fails, the error bubbles up. You handle it immediately. There's no silent failure.
Go functions return errors explicitly. You check them immediately. This pattern is verbose by design. The community accepts the boilerplate because it makes the unhappy path visible. You never miss a failure.
Configuration in Go often uses the functional options pattern. Functions like googleai.WithAPIKey return a function that modifies a configuration struct. This keeps the constructor signature clean while allowing optional parameters. You chain the options to build the config.
If you pass a string where a context is expected, the compiler rejects the program with cannot use "string" (untyped string constant) as context.Context value in argument. The compiler enforces the contract. You handle the runtime.
Realistic example: embeddings and vector stores
Here's a realistic setup: initialize an embedder, connect to a vector store, and prepare for retrieval-augmented generation.
Here's how to create an embedder from an LLM client.
// initEmbedder creates a client and wraps it for vector generation.
func initEmbedder(ctx context.Context) (embeddings.Embedder, error) {
apiKey := os.Getenv("GEMINI_API_KEY")
// Configure the client with the API key and embedding model.
client, err := googleai.New(ctx,
googleai.WithAPIKey(apiKey),
googleai.WithDefaultEmbeddingModel("text-embedding-004"),
)
if err != nil {
return nil, err
}
// The embedder interface standardizes text-to-vector conversion.
return embeddings.NewEmbedder(client)
}
Here's how to connect the vector store using the embedder.
// connectStore initializes the vector database with the embedder.
func connectStore(ctx context.Context, emb embeddings.Embedder) (*weaviate.Store, error) {
// Weaviate needs the embedder to generate vectors for new documents.
store, err := weaviate.New(
weaviate.WithEmbedder(emb),
weaviate.WithScheme("http"),
weaviate.WithHost("localhost:9035"),
weaviate.WithIndexName("Documents"),
)
if err != nil {
return nil, err
}
return store, nil
}
Vector stores are stateful. Test your connection before you trust the data.
Pitfalls and conventions
LLM calls are network calls. They can hang. Always pass a context with a deadline or cancellation channel. If you spawn a goroutine for an LLM call and the context isn't cancelled, the goroutine leaks. Goroutine leaks happen when the goroutine waits on a channel that never gets closed. Always have a cancellation path.
Embedding models produce vectors of specific dimensions. If you switch from a 768-dimension model to a 1536-dimension model, the vector store might reject the new vectors. Validate the dimension compatibility before you ingest data. Some vector stores create the schema on the first write. If you write with the wrong dimensions, you have to delete the index and start over.
The underscore discards a value intentionally. result, _ := ... says "I considered the second return value and chose to drop it". Use it sparingly with errors. Dropping an error without checking it hides bugs. The compiler warns you if you assign an error to _ in a function that returns an error, but it doesn't stop you in main.
Public names start with a capital letter. Private names start lowercase. There are no keywords like public or private. If you export a function, it's visible to other packages. If you keep it lowercase, it's private to the package. This convention keeps your API surface clean.
Forget to import a package and you get undefined: pkg from the compiler. Forget to use one and you get imported and not used. The compiler catches these immediately. Trust the tool.
Context is plumbing. Run it through every long-lived call site.
Decision: when to use LangChainGo
Use LangChainGo when you need to swap LLM providers without rewriting chain logic. Use LangChainGo when you want a unified interface for embeddings and vector stores across different backends. Use LangChainGo when you're building a prototype and want to iterate quickly on prompt chains and memory. Use the official provider SDK directly when you need fine-grained control over API parameters that the abstraction hides. Use raw HTTP calls when you're building a minimal proxy and don't want the dependency overhead. Use a custom wrapper when the standard interfaces don't match your domain model.
The simplest thing that works is usually the right thing.