# neo4j-go-driver connects Go services to Neo4j over Bolt

> The official Neo4j Go driver exposes Bolt connectivity, connection pooling, typed result handling and a preview feature channel, with a separate module path for every major version.

**neo4j/neo4j-go-driver** — Neo4j Bolt Driver for Go

- Repository: https://github.com/neo4j/neo4j-go-driver
- Stars: 555 · Forks: 74
- Language: Go
- License: Apache-2.0
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/neo4j-neo4j-go-driver

## One module path per major version, and the path changes the import

The neo4j-go-driver is the official Go driver for Neo4j, and the first practical detail is that its import path encodes the major version. There are four supported lines, and each has its own module path, so the code that imports the driver and the code that installs it have to agree. A project on the 6.x line imports github.com/neo4j/neo4j-go-driver/v6/neo4j, a project on 5.x uses the v5 path, and a project still on 4.x uses v4. The oldest line is the exception that proves the rule: for 1.x releases the path has no version suffix at all, and the README flags that absence explicitly so you do not paste a v4, v5 or v6 style path into an old project.

The module requires a Go project layout, since a go.mod file is expected in the application root, and the module file for the current line declares go 1.24. Installing the current line is a single command:

```shell
go get github.com/neo4j/neo4j-go-driver/v6
```

The 5.x and 4.x lines are installed the same way with their own path suffix, and the 1.x line uses the unsuffixed path. The repository's default branch is 6.x, and the release history shows that both 6.x and 5.x are being published, with v5.28.6 and v6.3.0 among the recent tags. In practice that means a 5.x project is not stranded, but the two lines do move separately.

## The driver only works with Go versions that are still maintained

There is a precondition that is easy to miss and awkward to discover late. The README states that the Go driver only works with actively maintained Go versions, and points at the official Go release policy for the definition. In an era when a Go release is supported for a defined window, that is a statement about the build toolchain and not about the database.

The practical effect is on your continuous integration rather than on your application code. A build agent pinned to an old Go image, a base image in a Dockerfile that nobody has updated in a year, or a developer machine on an unmaintained toolchain will fail on the driver even if nothing else changed. The module file's go 1.24 directive sets the floor for the current line, and the maintained-versions requirement sets the ceiling. Anyone planning a version upgrade of the driver should check the Go release policy first, because the two constraints move independently and the driver upgrade is the easier of the two to schedule.

## One driver per application, because the pool lives inside it

The connection model is where this driver asks you to change habits compared with a typical SQL client. Each Driver instance maintains a pool of connections internally, so the recommendation is to use one driver per application and tie the driver's lifetime to the application's lifetime. Creating a driver per request would mean creating a pool per request.

The inverse applies to sessions and transactions. Creating them is cheap, because a session or transaction does not open a new connection as long as the pool has a free connection available. The split of responsibilities is therefore clear: one long-lived driver, many short-lived sessions and transactions.

Thread safety follows a different line. The driver is thread-safe, while a session or a transaction is not. Sharing a session across goroutines is a data race waiting to happen, and the driver will not stop you. The context handling in the example code reflects the same design: since 5.0 you control the execution of most driver APIs through a context, and the sample uses a never-cancelling context for simplicity. Close is deferred with an explicit note to handle errors during deferred calls, which is the correct place to shut the pool down.

## Cluster routing needs the neo4j scheme and a core member

Talking to a causal cluster is not a separate API in this driver. You use the neo4j URL scheme and set the host to one of the core members of the cluster, and the driver handles the rest of the routing. The construction is the same NewDriver call used for a single server, with the URI and the basic auth credentials:

```go
driver, err := neo4j.NewDriver(
    "neo4j://localhost:7687",
    neo4j.BasicAuth("username", "password", ""),
)
```

The single-server example in the README uses the same scheme with a default port of 7687, spelled out in a comment next to the URI, so the difference between a standalone server and a cluster entry point is the host you name rather than a different constructor or a separate client type.

One warning belongs next to this. The README asks you to make sure the configuration in the code matches the version of the Neo4j server you run. That is not a formality for anyone operating a cluster with mixed server versions across an upgrade, since a driver configured for a newer server can refuse to work against the older members it routes to.

## Two ways to read results: eager transformer or streamed records

A Cypher execution result is a stream of records followed by a result summary, and the driver offers two ways to consume it. The quick path is neo4j.ExecuteQuery with a result transformer, and the sample passes neo4j.EagerResultTransformer, which collects the result so the code can index into result.Records directly and pull values out of the first record.

The other path is the streaming interface, and its error handling has a subtlety worth spelling out. When Next returns false you cannot assume the stream simply ended, so you have to check Err afterwards to tell the difference between a finished result and a failure that cut the consumption short. Values inside a record are read either by position, with the result typed as any so you convert it yourself, or by alias:

```go
value := record.Values[0]
```

A record access by alias returns a second boolean telling you whether the alias was found at all:

```go
if value, ok := record.Get('field_name'); ok {
    // a value with alias field_name was found
    // process value
}
```

The eager path suits a short query whose rows you need immediately. The streaming path suits large results, and its extra step is the price of knowing whether a short read was a clean end or a broken one.

## Cypher types become a small, explicit set of Go types

The driver surfaces values as any, which means the type has to be recovered somewhere. The mapping between Cypher types and the Go types used to represent them is small enough to hold in your head: null becomes nil, a list becomes []any, a map becomes map[string]any, a boolean becomes bool, an integer becomes int64, a float becomes float, a string becomes string, a byte array becomes []byte, and a node becomes neo4j.Node.

The sample code shows the intended path for pulling structured data out of that. neo4j.GetRecordValue[neo4j.Node] takes a record and an alias and yields a typed value, and neo4j.GetProperty[int64] or neo4j.GetProperty[string] then pulls a typed property out of the node, each returning an error when the value is missing or of the wrong type. That combination is the reason to pick this driver over raw map handling: the conversions are checked at the call site rather than left to a type assertion that panics at runtime.

The cost of the strictness is that an integer arriving from Cypher is int64 and not int, so a struct field expecting a plain int needs an explicit conversion, as the sample's Item type shows with an Id of type int64.

## Patch releases are rare, minor versions carry the fixes

The upgrade policy is unusual enough to plan around. The README states that patch versions will not be released except on rare occasions, that bug fixes and updates go into the latest minor version, and that users should upgrade to that. So a project pinned to 5.28.x should expect to move to 5.29.x when a fix lands, rather than waiting for a 5.28.7 to appear.

The compensating promise is about compatibility inside a major version. Driver upgrades within a major version will never contain breaking API changes, which means moving from 5.x to 5.y or 6.x to 6.y does not force code changes. What it does not promise is anything about crossing a major boundary, and that is what the MIGRATION_GUIDE.md file in the repository is for. The README links to it for anyone moving between previous versions of the driver.

The maintained-lines question is separate from the policy question. The release list shows v5.28.6 published alongside v6.3.0, so 5.x still receives work while 6.x is the default branch of the repository, and the supported versions knowledge base is the reference the README points to for how driver and server versions line up.

## Preview features change shape without a deprecation cycle

The driver runs a preview channel, and the README explains the rules plainly. A preview feature is a candidate for future GA status. It is not experimental, temporary or unstable, but it may change more rapidly and without the usual deprecation cycle, and most preview features are expected to reach GA unless something unexpected turns up. User feedback is encouraged while that decision is open, and every preview feature gets a dedicated GitHub Discussion for first impressions.

The practical implication for a service team is that a preview feature is stable enough to ship and unstable enough that you should read the discussion thread before you depend on a particular signature. Unlike a deprecation cycle, nothing warns you in advance.

The wider repository shape backs the driver. Alongside the neo4j package there are the benchkit and benchmark directories, the testkit and testkit-backend directories used for conformance testing, a test-stress directory, hooks, a generate-changelog.sh script, CONTRIBUTING.md and ISSUE_TEMPLATE.md, and a LICENSE file. The project is licensed Apache-2.0, and the last push to the repository was on 2026-10-20.

## Conclusion

Choose neo4j-go-driver if your Go service talks to Neo4j and you want the vendor's own Bolt implementation, with typed value access and one driver instance per application. Do not pick it if your Go toolchain is not actively maintained, since the driver refuses those versions, and treat the preview feature channel as a place where API shape can move without a deprecation cycle. Before you commit, check three things: which major version your server expects, since the module path carries the version and the default branch here is 6.x while 5.x patches continue, that your Neo4j server version matches the driver configuration you use, and that the one-driver-per-application rule holds in your code, because a session is not thread-safe while the driver is. The last push was on 2026-10-20 and the licence is Apache-2.0.

## FAQ

### How do I add the neo4j Go driver to my project?

Add it with `go get github.com/neo4j/neo4j-go-driver/v6` for the 6.x line, `go get github.com/neo4j/neo4j-go-driver/v5` for 5.x, and `go get github.com/neo4j/neo4j-go-driver/v4` for 4.x. For the 1.x line the module path has no version suffix.

### How many driver instances should a Go application create?

One per application. Each Driver instance keeps a pool of connections inside it, while sessions and transactions are cheap to create since they reuse free pool connections. The driver is thread-safe, but a session or transaction is not.

### How do I connect to a Neo4j causal cluster from Go?

Use the `neo4j` URL scheme and point the host at one of the core members of the cluster, for example `neo4j://localhost:7687`. The README also asks that the configuration in your code match the version of the Neo4j server you run.

### How do I tell an empty result from a failed query in the Go driver?

Call `Err()` after `Next()` returns false. A false return means the stream ended, and `Err()` tells you whether that was the end of the result stream or an error that stopped the consumption.

### What Go types does the Neo4j driver return for Cypher values?

Cypher null becomes nil, List becomes []any, Map becomes map[string]any, Boolean becomes bool, Integer becomes int64, Float becomes float, String becomes string, ByteArray becomes []byte and Node becomes neo4j.Node. Values are exposed as any, so typed helpers such as `neo4j.GetProperty[int64]` are used to convert them.

### How often are patch versions of the neo4j-go-driver released?

Rarely. The policy is that patch versions are not released except on rare occasions, with bug fixes and updates going into the latest minor version. Upgrades within a major version never contain breaking API changes.

## Sources

- [Issues](https://github.com/neo4j/neo4j-go-driver/issues)
- [License: Apache-2.0](https://github.com/neo4j/neo4j-go-driver/blob/6.x/LICENSE)
- [neo4j/neo4j-go-driver on GitHub](https://github.com/neo4j/neo4j-go-driver)
- [README](https://github.com/neo4j/neo4j-go-driver/blob/6.x/README.md)
- [Releases](https://github.com/neo4j/neo4j-go-driver/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/neo4j-neo4j-go-driver
