# go-libvirt: driving libvirt from Go over its own RPC protocol

> go-libvirt is a pure Go client for libvirt that speaks the XDR based RPC protocol instead of linking the C bindings, with the whole API generated from libvirt's own sources. It removes cgo from your build, at the cost of an unstable API that the README tells you to vendor, and of generated code tied to one libvirt version.

**digitalocean/go-libvirt** — Package libvirt provides a pure Go interface for interacting with Libvirt.  Apache 2.0 Licensed.

- Repository: https://github.com/digitalocean/go-libvirt
- Stars: 1,101 · Forks: 146
- Language: Go
- License: Apache-2.0
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/digitalocean-go-libvirt

## RPC and XDR instead of cgo and the C library

go-libvirt provides a pure Go interface for interacting with libvirt, and the README is explicit about how that is achieved. Rather than using libvirt's C bindings, the package makes use of libvirt's RPC interface, and RPC packets are encoded using the XDR standard as defined by RFC 4506. Connections to the libvirt server may be local or remote, so the same code can drive a daemon on the same machine over a Unix socket or a host across the network. The practical consequence for a Go project is the build. Nothing here needs a C compiler, the libvirt headers, or cgo in the cross compilation matrix, and a static binary can be produced for a machine that has no libvirt development package installed. The cost sits on the other side of the wire, because the client and the daemon now have to agree on protocol details, on authentication and on which libvirt version is running. The repository layout reflects that division: `rpc.go` and `connect_uri.go` handle the transport and connection setup, `socket/` holds the socket plumbing, and the bulk of the API lives in generated files such as `libvirt.gen.go` and the two protocol files.

## The API is generated, and the generators are yours to run

The README explains the reason for code generation plainly: libvirt's RPC interface is quite extensive and changes from one version to the next, so the project uses a pair of code generators to build the Go bindings. The generators do not run automatically when you build the package, they are meant to be run manually any time you change the version of libvirt you are using, and a downloaded copy of go-libvirt comes with generated files corresponding to one particular libvirt version. The README is direct about the two failure modes that follow. If you use a newer libvirt than the generated code was built against, the generated code may be missing functions; if you use an older one, the package may expose extra functions that return unimplemented errors if you call them. The regeneration procedure is three steps, and it differs by libvirt version. For libvirt below v6.7.0 you unpack the sources, create a `build` directory, change into it and run `../autogen.sh`. For libvirt v6.7.0 and later the equivalent is `meson setup build`. You do not have to build libvirt itself, but the configure step has to run because it generates required files. Then you set the `LIBVIRT_SOURCE` environment variable to the directory where you unpacked libvirt and run `go generate ./...` from the go-libvirt directory, which runs both generators.

## Finding a function: drop the prefix, learn the type names

The README spends a paragraph on how to map a libvirt C function onto its Go equivalent, which is the part you will use most. The suggested route is to start from the libvirt API reference, decide which C function you need, and then look for the Go name. The transformation is a single rule: the `vir` prefix is dropped, because in Go the functions live inside the package namespace, so `virDomainShutdown()` becomes `DomainShutdown()`, and you will find it in libvirt.gen.go with parameters and return values equivalent to those in the API reference. The second half of the mapping concerns flags, and it is the more interesting piece of design. In C these are plain integer arguments, and only the documentation ties them to an enum such as `virDomainShutdownFlagValues`, so the generator usually emits a distinct Go type, `DomainShutdownFlagValues`, with typed constants, which means Go tooling can suggest the legal values while you are writing the call. The README shows what that looks like in godoc, with the constants `DomainShutdownDefault`, `DomainShutdownAcpiPowerBtn`, `DomainShutdownGuestAgent`, `DomainShutdownInitctl`, `DomainShutdownSignal` and `DomainShutdownParavirt`, each annotated with the header line in libvirt where the enumeration is declared.

## Connecting and listing domains

The example in the README is the shortest useful program: parse a URI for the driver, connect, ask the server for its version, then list the domains. The code below is taken from the example, and it shows the shape of the API, where the receiver is the `*Libvirt` connection and every call returns an error you are expected to handle.

```go
package main

import (
	"fmt"
	"log"
	"net/url"

	"github.com/digitalocean/go-libvirt"
)

func main() {
	uri, _ := url.Parse(string(libvirt.QEMUSystem))
	l, err := libvirt.ConnectToURI(uri)
	if err != nil {
		log.Fatalf("failed to connect: %v", err)
	}

	v, err := l.ConnectGetLibVersion()
	if err != nil {
		log.Fatalf("failed to retrieve libvirt version: %v", err)
	}
	fmt.Println("Version:", v)

	flags := libvirt.ConnectListDomainsActive | libvirt.ConnectListDomainsInactive
	domains, _, err := l.ConnectListAllDomains(1, flags)
	if err != nil {
		log.Fatalf("failed to retrieve domains: %v", err)
	}

	fmt.Println("ID\tName\t\tUUID")
```

Two details are worth pulling out. The driver is named through a constant, `libvirt.QEMUSystem`, which is itself a generated value, so the same mechanism that produces the functions also produces the driver names. And the list call takes a flags argument built by combining `ConnectListDomainsActive` and `ConnectListDomainsInactive` with a bitwise or, which is the typed enumeration pattern again, this time for connection flags rather than domain flags. Once the connection is open the calls read like the C API with the prefix removed, and the README's own advice for finding the rest is the libvirt API reference plus the generated file.

## Prefer the generated routines over the hand written wrappers

Most of the code in go-libvirt is generated, but the README notes that a few hand written routines still exist in libvirt.go, and that they wrap calls to the generated code with slightly different parameters or return values. The project asks you to avoid those and to call the generated routines in libvirt.gen.go instead, and it says that over time the hand written routines will be removed. That is a deprecation notice with an unusual shape, since the wrappers are not marked deprecated in a doc comment, so nothing in your build will warn you. The practical rule for a consumer is simple: treat libvirt.gen.go as the API and libvirt.go as a compatibility layer with a short future. A related detail is the `libvirt.yml` file at the repository root, which is the machine readable description the generators consume, alongside `const.gen.go` and `const_strings.gen.go` for the enumerations and their string forms, and `qemu_protocol.gen.go` and `remote_protocol.gen.go` for the two protocol variants. If you ever need to know why a function has a particular signature, the specification file is where that question is answered, rather than the Go code.

## An unstable API that wants to be vendored

The README has a Warning section, and it is short enough to quote in substance. The packages are described as reasonably well tested and as having seen some use inside DigitalOcean, but there may be subtle bugs that could cause them to act in unexpected ways, with a use at your own risk caveat. More consequential is the second part: the API is not considered stable at this time, and if you want to include the package in a project, the project highly recommends vendoring it. That is an unusual request to put in a library README, and it explains a lot about how this code is meant to be consumed. Since the repository publishes no GitHub releases, there is no version series to depend on, so a vendored copy or a pinned commit is the only way to make the dependency reproducible, and an upgrade becomes a deliberate act where you re-run the generators and review the diff. The licence is Apache 2.0, the module targets Go 1.24.0, and the dependencies are modest, with testify for the tests, x/crypto and x/tools, the latter being what the code generators use, and yaml.v3 pulled in indirectly by the tooling.

## How it differs from using the C bindings directly

The alternative is the obvious one: use libvirt's own C bindings from Go through cgo, or shell out to the virsh command line tool. The difference in approach is where the coupling sits. The C binding route puts a native dependency in your build: a C toolchain, the libvirt development headers, and a version of libvirt that matches what your server runs, all of which have to be present on every machine that builds and every runtime image that ships the binary. The RPC route removes that coupling from the build and replaces it with a runtime dependency on a reachable libvirt daemon and on both sides agreeing on the protocol. That trade is favourable for a Go service with a simple deployment story and unfavourable when you need the very latest libvirt features, because the generated bindings are pinned to the version they were generated against and the README tells you to regenerate them yourself. The shell approach sits in between, with no build coupling and no protocol coupling, at the cost of parsing command output and losing type safety, and it is the pragmatic choice for scripts rather than applications.

## Conclusion

Adopt go-libvirt if you build Go tooling that manages virtual machines or networks through libvirt and you want to drop cgo and the C headers from your build and release pipeline, and if you can pin a commit and re-run the generators when libvirt moves. Do not adopt it for a service that needs a stable public API, because the README states plainly that the API is not considered stable and recommends vendoring, and it warns of subtle bugs with its own use at your own risk disclaimer. Before you commit, generate the bindings against the exact libvirt version your server runs, following the meson or autogen step for that version, and remember that a downloaded copy of the package may be missing functions for a newer libvirt or may expose functions that return unimplemented errors. The licence is Apache 2.0, the repository publishes no GitHub releases, and the last push was on 2026-10-13.

## FAQ

### Does go-libvirt need cgo or the libvirt C headers?

No. The README says the package makes use of libvirt's RPC interface rather than libvirt's C bindings, with RPC packets encoded using the XDR standard from RFC 4506. Connections can be local or remote.

### How do I regenerate the go-libvirt bindings for a newer libvirt?

Unpack the libvirt sources, run the configure step, which is `meson setup build` for libvirt v6.7.0 and later or `../autogen.sh` in a build directory for older versions, set the `LIBVIRT_SOURCE` environment variable to the libvirt directory, then run `go generate ./...` from the go-libvirt directory.

### How do I find the Go function for a libvirt C function?

The `vir` prefix is dropped, so `virDomainShutdown()` becomes `DomainShutdown()` in libvirt.gen.go, with parameters and return values matching the libvirt API reference. Flag arguments usually get a distinct Go type such as `DomainShutdownFlagValues`, with typed constants you can list in godoc.

### Is the go-libvirt API stable?

The README says the API is not considered stable at this time and highly recommends vendoring the package into your project. It also warns that there may be subtle bugs and says to use it at your own risk, and the repository publishes no GitHub releases.

## Sources

- [digitalocean/go-libvirt on GitHub](https://github.com/digitalocean/go-libvirt)
- [Issues](https://github.com/digitalocean/go-libvirt/issues)
- [License: Apache-2.0](https://github.com/digitalocean/go-libvirt/blob/main/LICENSE)
- [README](https://github.com/digitalocean/go-libvirt/blob/main/README.md)

---

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