Library / SDK
warthog618/go-gpiocdev avatar
warthog618/go-gpiocdev

go-gpiocdev talks to the Linux GPIO character device without libgpiod

A native Go library for accessing GPIO lines on Linux platforms using the GPIO character device

521 stars46 forksGoMIT

At a glance

What is it?
go-gpiocdev is a Go library for driving GPIO lines through the Linux GPIO character device, and it is the Go equivalent of the C libgpiod library in functionality even though it does not mirror its API. It suits a Go program that needs to read a button or drive a relay, and it needs Linux 5.10 or newer.
Who is it for?
go-gpiocdev is worth using if your Go program needs to drive GPIO lines and you would rather not link a C library, since the whole feature set of libgpiod is available through a Go API and the uAPI v2 ioctl path means no cgo in the build.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 12 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.

Editorial analysis

It is the Go equivalent of libgpiod, but not a mirror of it

The framing in the documentation is precise, and the distinction is the first thing to understand. The goal of the library is to provide the Go equivalent of the C libgpiod library, and the intent is explicitly not to mirror the libgpiod API but to provide the equivalent functionality. That is a real design decision rather than a caveat. A direct port would carry libgpiod's naming, its error handling conventions and its chip and line abstractions into Go, and a Go developer would then be reading C documentation. This library instead assumes the reader knows Go and gives them options, functions and errors in the idiom of the language. The package name is `gpiocdev` and the import path is `github.com/warthog618/go-gpiocdev`, the module is MIT licensed, and the repository carries a `LICENSES/` directory alongside the LICENSE file, which is the pattern used for a project that vendors code under more than one licence.

uAPI v2 only, which fixes the kernel floor at 5.10

The kernel requirement is stated without ambiguity, and it comes with a documented escape route. The library uses Linux GPIO uAPI v2 and so requires Linux 5.10 or later. Older versions of gpiocdev, up to v0.9.x, supported both versions of the uAPI and can still be used if you are stuck on a kernel older than Linux 5.10. The reason for the split is the direction the kernel took. The character device interface was reworked so that lines are requested as objects through the ioctl rather than through a legacy global request model, and a library that supports the new model cannot also speak the old one in the same call path. The practical consequence is that on a modern distribution the choice is made for you, and on an embedded board with an older kernel the older release is the one to reach for. The go.mod file shows the dependency footprint is tiny, requiring only the testify assertion library, a GPIO simulator used in tests, and golang.org/x/sys for the syscall layer.

A line must be requested before it can be read or written

The resource model is the thing a newcomer has to internalise, and the documentation is explicit that a line must be requested first. The request names the chip and the offset:

go
l, _ := gpiocdev.RequestLine("gpiochip0", 4)   // in its existing state

The offset parameter identifies the line on the chip and is specific to that GPIO chip. The same call can be made from a Chip object, which is the form to use when you are working with several lines on one chip:

go
l, _ := c.RequestLine(4)                    // from a Chip object

For readability, convenience mappings can be provided for specific devices, such as the Raspberry Pi, where the offset is a named constant rather than a number:

go
l, _ := c.RequestLine(rpi.J8p7)             // using Raspberry Pi J8 mapping

The initial state of the line is set through configuration options, so the same request can ask for an output line rather than an input:

go
l, _ := gpiocdev.RequestLine("gpiochip0", 4, gpiocdev.AsOutput(1))  // as an output line

When the lines are no longer needed they must be closed to release the resources, and the documentation shows the calls for both a single line and a collection.

Collections set and read their lines in one operation

Multiple lines from the same chip can be requested as a collection, and the collection API is where the library saves real work. The request takes the chip, the offsets and a configuration option per line:

go
ll, _ := gpiocdev.RequestLines("gpiochip0", []int{0, 1, 2, 3}, gpiocdev.AsOutput(0, 0, 1, 1))

or from a Chip object:

go
ll, _ := c.RequestLines([]int{0, 1, 2, 3}, gpiocdev.AsOutput(0, 0, 1, 1))

The per-line configuration values in that option are what allows different configurations for lines within a collection, one of the listed features. Reading and writing follow the same batched shape. A collection reads all its levels at once into a caller supplied buffer:

go
rr := []int{0, 0, 0, 0} // buffer to read into...
ll.Values(rr)           // Read the state of a collection of lines

and writes all of them in a single call:

go
ll.SetValues([]int{0, 1, 0, 1}) // Set a collection of lines

For a single line the operations are `l.Value()` to read the state as active or inactive, and `l.SetValue(1)` or `l.SetValue(0)` to set the line active or inactive. A set of examples under the `examples/` directory shows each of these in isolation, including `toggle_line_value`, `watch_line_value` and `get_multiple_line_values`.

Edge watches are serial, and the handler is expected to be short

The event model has one rule that decides how you must write your handler. The value of an input line can be watched and trigger calls to handler functions, and the watch can be on rising edges, falling edges, or both, with the handler passed through the `WithEventHandler(eh)` option. The handler receives a `LineEvent` containing details of the edge event including the offset of the triggering line, the time the edge was detected and the type of edge detected:

go
func handler(evt gpiocdev.LineEvent) {
  // handle edge event
}

l, _ = c.RequestLine(rpi.J8p7, gpiocdev.WithEventHandler(handler), gpiocdev.WithBothEdges)

To maintain event ordering, the event handler is called serially from a goroutine that reads the events from the kernel, and the handler is expected to be short lived, handing off any potentially blocking operations to a separate goroutine. Two consequences follow. A slow handler does not just delay one event, it delays every later event, because the reader goroutine is serial. And a watch can be removed by closing the line, or by reconfiguring the requested lines to disable edge detection with `l.Reconfigure(gpiocdev.WithoutEdges)`. The Close call waits for the event handler to return, so it must not be called from the event handler context and should be called from a separate goroutine.

The feature list is per line, and the options are the API

The listed capabilities are worth reading line by line, because they are the contract. The library supports direction, meaning input or output, write, read, active high or low with a default of high, output mode in the forms push-pull, open-drain and open-source, pull up or pull down, watches and edge detection on rising, falling or both edges, chip and line labels, debouncing of input lines, and different configurations for lines within a collection. Configuration is set through options passed to `Chip.RequestLine(s)` and to `Line.Reconfigure`, and those options override any default which may be set in `NewChip`. The mechanism is functional options, so a request reads as a call with named behaviour rather than a struct of flags, and the same option set applies whether you are configuring a single line or reconfiguring one later. The repository layout reflects the split, with `options.go` and its test file, `watcher.go` and `infowatcher.go` for the event side, the `uapi/` directory holding the kernel interface, and `device/` for the device layer.

Goroutine safety is claimed, and the benchmark lives in the root

Two properties are stated that a user of a hardware library will care about. The first is that all library functions are safe to call from different goroutines, which is unusual to claim for a library that owns a file descriptor and a watch. The second is that all of the files in the repository root are small and focused, with `gpiocdev.go` as the main entry, `options.go` for the configuration surface, `watcher.go` and `infowatcher.go` for edge and information watching, and the tests and a benchmark alongside. The benchmark file is named `gpiocdev_bmark_test.go`, so the performance measurement lives with the code rather than in a separate directory, and the `Makefile` shows the build is a plain `go build` with the version stamped in through a linker flag. The Makefile also builds every example, deriving the list from the `examples/*/main.go` files with a wildcard, so a new example is picked up by the build without editing the file.

Where a GPIO character device library is the wrong choice

Two limits are worth naming. The first is the platform, and it is absolute. The library talks to the Linux GPIO character device, so it does nothing on macOS or Windows, and even on Linux it depends on a kernel and a board combination that expose a GPIO chip at all, which rules out a class of embedded boards entirely. The second is the arbitration question, and the documentation is silent on it. The character device model means a line is an object requested by a process, and the kernel tracks which process holds which line. What happens when two processes, or two goroutines in the same process, both want the same line is not described in the visible documentation, and the fact that the library exposes `Chip` and `Chip.RequestLine` suggests the intended pattern is one chip object per consumer rather than a shared registry. The alternative to weigh against is the sysfs interface, which is what a great many GPIO tutorials still use, and which requires no character device and no uAPI version at all. The difference in approach is that sysfs gives you one file per line and no structure, while the character device gives you a coherent object model with batches, watches and options, at the cost of requiring a 5.10 kernel and a chip abstraction that some boards do not implement. For a Raspberry Pi or a similar board on a current kernel, the character device is the better interface. For an old embedded kernel or a board with no v2 support, the older gpiocdev release is the answer the documentation gives you.

Editorial conclusion

go-gpiocdev is worth using if your Go program needs to drive GPIO lines and you would rather not link a C library, since the whole feature set of libgpiod is available through a Go API and the uAPI v2 ioctl path means no cgo in the build. It is a poor fit if you are stuck on a kernel older than Linux 5.10, because the older releases support both uAPI versions and the current ones do not, and a poor fit for a project that needs multi-chip or multi-process line arbitration, because the documentation does not describe how the library handles contention. Before you adopt it, confirm your kernel version and that the character device interface is available, and note that the last push to the repository was on 2026-10-11 with no tagged releases.

Frequently asked questions

What is go-gpiocdev and how does it relate to libgpiod?

It is a native Go library for accessing GPIO lines on Linux platforms using the GPIO character device, and the goal is to provide the Go equivalent of the C libgpiod library. The intent is explicitly not to mirror the libgpiod API but to provide the equivalent functionality in a Go idiom.

What kernel version does go-gpiocdev require?

The library uses Linux GPIO uAPI v2 and so requires Linux 5.10 or later. Older versions of gpiocdev, up to v0.9.x, supported both versions of the uAPI and can still be used if you are stuck on a kernel older than Linux 5.10.

How do I read and write a GPIO line with go-gpiocdev?

You request the line first, naming the chip and the offset, for example gpiocdev.RequestLine("gpiochip0", 4), optionally with gpiocdev.AsOutput(1) to set its initial state. Then you read it with l.Value() and set it with l.SetValue(1) or l.SetValue(0), and you close the line when you are done with it to release the resources.

How do edge watches work in go-gpiocdev?

You pass a handler with the WithEventHandler option and choose rising, falling or both with WithBothEdges. The handler receives a LineEvent with the offset of the triggering line, the time the edge was detected and the edge type, and it is called serially from a goroutine reading events from the kernel, so it is expected to be short lived and to hand blocking work to another goroutine. A watch is removed by closing the line or by reconfiguring with gpiocdev.WithoutEdges.

Official sources

  1. Issues
  2. License: MIT
  3. README
  4. warthog618/go-gpiocdev on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/warthog618-go-gpiocdev.svg)](https://hysenlabs.com/projects/warthog618-go-gpiocdev)

Was this useful?