When to Use
- Writing Go code that connects to Neo4j
- Setting up , , or session/transaction patterns
- Debugging connection errors, result iteration, type assertions, causal consistency
When NOT to Use
- Writing/optimizing Cypher →
- v5→v6 migration steps →
Installation
bash
go get github.com/neo4j/neo4j-go-driver/v6
Import:
github.com/neo4j/neo4j-go-driver/v6/neo4j
v5→v6 rename (deprecated aliases still compile, remove before v7):
| v5 | v6 |
|---|
neo4j.NewDriverWithContext(...)
| |
| |
Environment Variables
go
import "os"
uri := getEnv("NEO4J_URI", "neo4j://localhost:7687")
user := getEnv("NEO4J_USERNAME", "neo4j")
password := getEnv("NEO4J_PASSWORD", "")
database := getEnv("NEO4J_DATABASE", "neo4j")
func getEnv(key, fallback string) string {
if v := os.Getenv(key); v != "" { return v }
return fallback
}
Use
godotenv to load
in dev:
.
in
.
Driver Lifecycle
One
per application. Goroutine-safe, connection-pooled, expensive to create.
go
func NewNeo4jDriver(uri, user, password string) (neo4j.Driver, error) {
driver, err := neo4j.NewDriver(
uri, // "neo4j+s://xxx.databases.neo4j.io" for Aura
neo4j.BasicAuth(user, password, ""),
)
if err != nil {
return nil, fmt.Errorf("create driver: %w", err)
}
ctx := context.Background()
if err := driver.VerifyConnectivity(ctx); err != nil {
driver.Close(ctx)
return nil, fmt.Errorf("verify connectivity: %w", err)
}
return driver, nil
}
// In main / app teardown:
defer driver.Close(ctx)
❌ Never create driver per-request. Create once at startup; share across goroutines.
URI schemes:
(Aura/TLS+routing),
(plain+routing),
(TLS+single),
(plain+single).
Choosing the Right API
| API | Use when | Auto-retry | Lazy results |
|---|
| Most queries — simple default | ✅ | ❌ eager |
session.ExecuteRead/Write()
| Large result sets / streaming | ✅ | ✅ |
session.BeginTransaction()
| Spans multiple functions / ext coordination | ❌ | ✅ |
| / auto-commit only | ❌ | ✅ |
CALL { … } IN TRANSACTIONS
and
manage their own transactions — use
. They fail inside managed transactions.
ExecuteQuery (Recommended Default)
Manages sessions, transactions, retries, and bookmarks automatically.
go
result, err := neo4j.ExecuteQuery(ctx, driver,
`MATCH (p:Person {name: $name})-[:KNOWS]->(friend)
RETURN friend.name AS name`,
map[string]any{"name": "Alice"},
neo4j.EagerResultTransformer,
neo4j.ExecuteQueryWithDatabase("neo4j"), // always specify
neo4j.ExecuteQueryWithReadersRouting(), // for read queries
)
if err != nil {
return fmt.Errorf("query people: %w", err)
}
for _, record := range result.Records {
name, _ := record.Get("name")
fmt.Println(name)
}
fmt.Println(result.Summary.Counters().NodesCreated())
Key options:
go
neo4j.ExecuteQueryWithDatabase("mydb") // required for performance
neo4j.ExecuteQueryWithReadersRouting() // route reads to replicas
neo4j.ExecuteQueryWithImpersonatedUser("jane") // impersonate
neo4j.ExecuteQueryWithoutBookmarkManager() // opt out of causal consistency
❌ Never concatenate user input into query strings. Always use
parameters.
Managed Transactions (Session-Based)
Use for lazy streaming (large result sets) or callback-level control.
go
session := driver.NewSession(ctx, neo4j.SessionConfig{
DatabaseName: "neo4j", // always specify
AccessMode: neo4j.AccessModeRead,
})
defer session.Close(ctx)
result, err := session.ExecuteRead(ctx,
func(tx neo4j.ManagedTransaction) (any, error) {
res, err := tx.Run(ctx,
`MATCH (p:Person) RETURN p.name AS name LIMIT $limit`,
map[string]any{"limit": 100},
)
if err != nil {
return nil, err
}
var names []string
for res.Next(ctx) { // lazy — don't Collect() on large sets
name, _ := res.Record().Get("name")
names = append(names, name.(string))
}
return names, res.Err()
},
)
❌ No side effects in callback — retried on transient failures.
→ replicas.
→ cluster leader.
Explicit Transactions
Use when transaction work spans multiple functions or requires external coordination.
go
session := driver.NewSession(ctx, neo4j.SessionConfig{DatabaseName: "neo4j"})
defer session.Close(ctx)
tx, err := session.BeginTransaction(ctx)
if err != nil {
return err
}
if err := doPartA(ctx, tx); err != nil {
tx.Rollback(ctx)
return err
}
if err := doPartB(ctx, tx); err != nil {
tx.Rollback(ctx)
return err
}
return tx.Commit(ctx)
❌ Not auto-retried. Caller handles retry. Prefer managed transactions unless you need explicit control.
Error Handling
go
result, err := neo4j.ExecuteQuery(...)
if err != nil {
var neo4jErr *neo4j.Neo4jError
if errors.As(err, &neo4jErr) {
slog.Error("database error", "code", neo4jErr.Code, "msg", neo4jErr.Msg)
}
var connErr *neo4j.ConnectivityError
if errors.As(err, &connErr) {
slog.Error("connectivity error", "err", connErr)
}
return fmt.Errorf("execute query: %w", err)
}
Helpers:
go
neo4j.IsNeo4jError(err) // server-side Cypher/database error
neo4j.IsTransactionExecutionLimit(err) // managed tx retries exhausted
In managed tx callback: return error → driver retries if transient.
at startup: check URI scheme, credentials, firewall.
Data Types
go
// Typed extraction (v6+, preferred):
neo4j.GetRecordValue[string](record, "name")
// Manual extraction:
rawAge, ok := record.Get("age")
if !ok { return errors.New("missing 'age' field") }
age := rawAge.(int64) // Neo4j integers → int64
// Node access:
rawNode, _ := record.Get("p")
node := rawNode.(neo4j.Node)
name := node.Props["name"].(string)
labels := node.Labels // []string
❌ Always check
from
before type-asserting — panics on missing key.
❌ After lazy
loop, always check
.
Key Patterns
Context — always propagate
go
ctx, cancel := context.WithTimeout(parentCtx, 5*time.Second)
defer cancel()
// pass ctx to all driver calls
has no deadline — slow queries block indefinitely.
Batching Writes
go
// Bad: one transaction per record
for _, item := range items {
neo4j.ExecuteQuery(ctx, driver, writeQuery, item, ...)
}
// Good: UNWIND batch in one transaction
neo4j.ExecuteQuery(ctx, driver,
`UNWIND $items AS item
MERGE (n:Node {id: item.id})
SET n += item`,
map[string]any{"items": items},
neo4j.EagerResultTransformer,
neo4j.ExecuteQueryWithDatabase("neo4j"),
)
Generic Helpers (v6+)
Prefer type-safe helpers over manual assertions:
go
// GetRecordValue[T] — extract + cast in one call
name, isNil, err := neo4j.GetRecordValue[string](record, "name")
// isNil=true when OPTIONAL MATCH returned null; err != nil when key absent or wrong type
// CollectTWithContext — map all records to a slice
people, err := neo4j.CollectTWithContext(ctx, result, func(record *neo4j.Record) (Person, error) {
name, _, err := neo4j.GetRecordValue[string](record, "name")
age, _, _ := neo4j.GetRecordValue[int64](record, "age")
return Person{Name: name, Age: int(age)}, err
})
// SingleTWithContext — expect exactly one record (error if 0 or 2+)
person, err := neo4j.SingleTWithContext(ctx, result, func(record *neo4j.Record) (Person, error) {
name, _, _ := neo4j.GetRecordValue[string](record, "name")
return Person{Name: name}, nil
})
// GetProperty — typed property from Node or Relationship
node, _, _ := neo4j.GetRecordValue[neo4j.Node](record, "p")
nameVal, err := neo4j.GetProperty[string](node, "name")
Spatial Types
go
// 2D Cartesian (SRID 7203), 3D Cartesian (SRID 9157)
pt2d := neo4j.Point2D{X: 1.23, Y: 4.56, SpatialRefId: 7203}
pt3d := neo4j.Point3D{X: 1.23, Y: 4.56, Z: 7.89, SpatialRefId: 9157}
// 2D WGS-84 (SRID 4326), 3D WGS-84 (SRID 4979)
london := neo4j.Point2D{X: -0.118092, Y: 51.509865, SpatialRefId: 4326}
shard := neo4j.Point3D{X: -0.0865, Y: 51.5045, Z: 310, SpatialRefId: 4979}
// Pass as parameter
result, err := neo4j.ExecuteQuery(ctx, driver,
"CREATE (p:Place {location: $loc})",
map[string]any{"loc": london},
neo4j.EagerResultTransformer,
neo4j.ExecuteQueryWithDatabase("neo4j"),
)
// Read from result — assert to Point2D or Point3D
raw, _ := record.Get("location")
if p2d, ok := raw.(neo4j.Point2D); ok {
fmt.Printf("lon=%f lat=%f srid=%d\n", p2d.X, p2d.Y, p2d.SpatialRefId)
}
// Distance (same SRID only)
result, _ = neo4j.ExecuteQuery(ctx, driver,
"RETURN point.distance($p1, $p2) AS distance",
map[string]any{"p1": pt2d, "p2": neo4j.Point2D{X: 10, Y: 10, SpatialRefId: 7203}},
neo4j.EagerResultTransformer, neo4j.ExecuteQueryWithDatabase("neo4j"),
)
dist, _ := result.Records[0].Get("distance")
fmt.Println(dist.(float64))
Always Specify Database
go
neo4j.ExecuteQueryWithDatabase("neo4j") // in ExecuteQuery
neo4j.SessionConfig{DatabaseName: "neo4j"} // in sessions
Omitting costs a network round-trip per call to resolve home database.
Causal Consistency
manages bookmarks automatically — no action needed for sequential calls.
Cross-session (parallel workers): combine bookmarks explicitly — see
references/repository-pattern.md.
Common Errors
| Error / Symptom | Cause | Fix |
|---|
| at startup | URI wrong / TLS mismatch / firewall | Check scheme ( for Aura), credentials, port 7687 |
| mid-run | Pool exhausted | Increase ; check for leaked sessions |
| Panic on type assertion | returned nil/wrong type | Use neo4j.GetRecordValue[T]()
or check first |
| non-nil after loop | Network error mid-stream | Handle error; re-run transaction |
| Callback retried unexpectedly | Side effect inside managed tx | Move side effects outside callback |
| Context deadline exceeded | No timeout on context | Use |
| 0 results, query looks correct | Wrong | Always set in config |
| fails | Run inside managed tx | Use (auto-commit) |
References
Load on demand:
Load on demand:
- references/advanced-config.md — connection pool tuning, custom address resolver, notification config, Bolt logging, auth options, URI scheme table
- references/repository-pattern.md — repository wrapper pattern, cross-session causal consistency with bookmarks
WebFetch
| Need | URL |
|---|
| Go driver manual | https://neo4j.com/docs/go-manual/current/
|
| API reference | https://pkg.go.dev/github.com/neo4j/neo4j-go-driver/v6/neo4j
|
Checklist