GopherCon 2019 - Simple, Portable and Efficient Graphical Interfaces in Go

Presenter: Elias Naur

Liveblogger: Christina Forney


Gio is a new open source Go library for writing immediate mode GUI programs that run on all the major platforms: Android, iOS/tvOS, macOS, Linux, Windows. The talk will cover Gio's unusual design and how it achieves simplicity, portability and performance.

Why GUIs?

Last year at GopherCon we asked what the biggest challenges that Go developers faced. Here are the results:

Go challenges graph

As you heard in the keynote, Modules, generics, and error handling is being handled by the Go team, so I wanted to focus on making writing GUIs in Go easy.


Gio -

  • Gio is a simple Go module for writing portable and fast graphical interfaces.

Scatter -

  • Scatter is a Gio program for end-to-end encrypted messaging over email.

Demo - Scatter

Scatter is a multi-platform messaging application for sending and receiving encrypted chat messages, implementing the Signal protocol over federated email.

Scatter UI


I wanted to be able to write a GUI program in GO that I could implement only once and have it work on every platform. This, to me, is the most interesting feature of Gio.


  • Immediate mode design.
    • UI state owned by program.
  • Only depends on lowest-level platform libraries.
    • Minimal dependency tree to keep things low level as possible.
  • GPU accelerated vector and text rendering.
    • It’s super efficient
  • No garbage generated in drawing or layout code.
  • Cross platform (macOS, Linux, Windows, Android, iOS, tvOS, Webassembly).
  • Core is 100% Go. OS-specific native interfaces are optional.

Immediate mode UI

Some programs require you to maintain state for your widgetry. In Gio, you draw what you need to draw, you layout what you need to layout, and that’s it!

  • UI state is owned by the program. Even layout and widget tree.
  • No callbacks. Events are handled while drawing.

Blank window

This is all you need to render a simple blank window:

package main
import (
func main() {
    go func() {
        w := app.NewWindow(nil)
        for range w.Events() {

This is odd, because you’re doing the event loop in your go routine.

Hello, World

Slightly more advanced example, but in this case you are loading up some support structures and adding text.Label to display your label:

func main() {
    go func() {
        w := app.NewWindow(nil)
        regular, _ := sfnt.Parse(goregular.TTF)
        var cfg ui.Config
        var faces measure.Faces
        ops := new(ui.Ops)
        for e := range w.Events() {
            if e, ok := e.(app.DrawEvent); ok {
                cfg = &e.Config
                cs := layout.RigidConstraints(e.Size)
                // ADD YOUR LABELS
                lbl := text.Label{Face: faces.For(regular, ui.Sp(72)), Text: “Hello, World!”}
                lbl.Layout(ops, cs)

Go challenges graph

Running Gio programs

Linux, macOS, Windows

Enable modules

export GO111MODULE=on

I recommend you enable for convenience and also because I break the API often, so you will be shielded from updates that could break your application until you are ready to upgrade.

Build, install or run the program

go build
go install
go run helloworld.go


There is a tool to package your application as an APK that you can install through the ads tool to run on a device or simulator.

Install the gio tool:

go install
$GOBIN/gio -target android -o hello.apk helloworld.go

Install on a connected device or emulator with adb:

adb install hello.apk


For iOS/tvOS devices:

$GOBIN/gio -target <ios|tvos> -o hello.ipa -appid <bundle id> helloworld.go

Use the .app file extension for simulators:

$GOBIN/gio -target <ios|tvos> -o helloworld.go

Install on a running simulator:

xcrun simctl install booted


To output a directory ready to serve:

$GOBIN/gio -target js -o www helloworld.go

Use a webserver or goexec to serve it:

go run ‘http.ListenAndServe(“:8080”, http.FileServer(http.Dir(“www”)))’

Compile directly with the Go tool or use the Gio tool to build as a web assembly module, but also add the necessary file to supply it to work in your browser.


The way you communicate each user interface update to gio. Gio has not state so you have to add it to every frame.

Operations buffer and type called ui ops and you add operations to that to your ops buffers which sends to window.draw method.


Serializing operations

import “” // Pure Go
var ops ui.Ops
// Add operations to ops

Only the app package depends on platform libraries

import “”
var w app.Window

Position other operations

import “”

Request a redraw

ui.InvalidateOp{}.Add(ops) // Immediate
ui.InvalidateOp{At: …}.Add(ops) // Delayed

Drawing operations

Set current color or image

import “”
draw.ColorOp{Color: color.RGBA{…}}.Add(ops)
draw.ImageOp{Src: …, Rect: …}.Add(ops)

Draw with the current color or image

draw.DrawOp{Rect: …}.Add(ops)

Clip operations

Clip drawing to a rectangle

import “”

Or to an outline

var b draw.PathBuilder
b.Quad(…) // Quadratic Beziér curve
b.Cube(…) // Cubic Beziér curve

Input operations

Keyboard and text input

import “”
// Declare key handler.
key.HandlerOp{Key: handler, Focus: true/false}.Add(ops)
// Hide soft keyboard.

Mouse and touch input

import “”
// Define hit area.
pointer.RectAreaOp{Size: …}.Add(ops)
pointer.EllipseAreaOp{Size: …}.Add(ops)
// Declare pointer handler.
pointer.HandlerOp{Key: c, Grab true/false}


Drawing (and animating)

Drawing and animating a clipped square

    square := f32.Rectangle{Max: f32.Point{X: 500, Y: 500}}
    radius := animateRadius(e.Config.Now(), 250)
    // Position
        X: 100,
        Y: 100,
    // Color
    draw.ColorOp{Color: color.RGBA{A: 0xff, G: 0xcc}}.Add(ops)
    // Clip corners
    roundRect(ops, 500, 500, radius, radius, radius, radius)
    // Draw
    draw.DrawOp{Rect: square}.Add(ops)
    // Animate
    // Submit operations to the window.


If you have non-trivial setup, you need some way to lay them out - you don’t want to use absolute coordinates for each item. Layout assembly helps you structure your user interface. As a result of calling their layout, widgets will give you their own size.

Constraints and dimensions

Constraints are input

package layout // import
type Constraints struct {
    Width  Constraint
    Height Constraint
type Constraint struct {
    Min, Max int

Dimensions are output

type Dimens struct {
    Size     image.Point
    Baseline int

Widgets accept constraints, output dimensions

package text // import
func (l Label) Layout(ops *ui.Ops, cs layout.Constraints) layout.Dimens
func (e *Editor) Layout(ops *ui.Ops, cs layout.Constraints) layout.Dimens
package widget // import
func (im Image) Layout(c ui.Config, ops *ui.Ops, cs layout.Constraints) layout.Dimens

Example - two labels

func drawLabels(face text.Face, ops *ui.Ops, cs layout.Constraints) {
    **cs.Height.Min = 0**
    lbl := text.Label{Face: face, Text: “One label”}
    **dimensions := lbl.Layout(ops, cs)**
        **Y: float32(dimensions.Size.Y),**
    lbl2 := text.Label{Face: face, Text: “Another label”}
    lbl2.Layout(ops, cs)

Layout helpers

Can layout to the compass directions or to specific place, like the center.


var ops *ui.Ops
var cs layout.Constraints
align := layout.Align{Alignment: layout.Center}
cs = align.Begin(ops, cs)

dimensions := someWidget.Layout(…, cs) // Draw widget

dimensions = align.End(dimensions)


var cfg ui.Config
inset := layout.Inset{Top: ui.Dp(8), …} // 8dp top inset
cs = inset.Begin(c, ops, cs)

dimensions := anotherWidget.Layout(…, cs) // Draw widget

dimensions = inset.End(dimensions)

Flex layout

Lay out widgets on an axis.

func drawRects(c ui.Config, ops *ui.Ops, cs layout.Constraints) {
    flex := layout.Flex{}
    flex.Init(ops, cs)
    cs = flex.Flexible(0.5)
    dimensions := drawRect(c, ops, color.RGBA{A: 0xff, R: 0xff}, cs)
    red := flex.End(dimensions)
    cs = flex.Flexible(0.25)
    dimensions = drawRect(c, ops, color.RGBA{A: 0xff, G: 0xff}, cs)
    green := flex.End(dimensions)
    cs = flex.Flexible(0.25)
    dimensions = drawRect(c, ops, color.RGBA{A: 0xff, B: 0xff}, cs)
    blue := flex.End(dimensions)
    flex.Layout(red, green, blue)

Stack layout

func drawRects(c ui.Config, ops *ui.Ops, cs layout.Constraints) {
    stack := layout.Stack{Alignment: layout.Center}
    stack.Init(ops, cs)
    cs = stack.Rigid()
    dimensions := drawRect(c, ops, color.RGBA{A: 0xff, R: 0xff}, ui.Dp(50), cs)
    red := stack.End(dimensions)
    cs = stack.Rigid()
    dimensions = drawRect(c, ops, color.RGBA{A: 0xff, G: 0xff}, ui.Dp(100), cs)
    green := stack.End(dimensions)
    cs = stack.Rigid()
    dimensions = drawRect(c, ops, color.RGBA{A: 0xff, B: 0xff}, ui.Dp(150), cs)
    blue := stack.End(dimensions)
    stack.Layout(red, green, blue)

List layout

        list := &layout.List{
            Axis: layout.Vertical,
func drawList(c ui.Config, q input.Queue, list *layout.List, face text.Face, ops *ui.Ops, cs layout.Constraints) {
    const n = 1e6
    for list.Init(c, q, ops, cs, n); list.More(); list.Next() {
        txt := fmt.Sprintf(“List element #%d", list.Index())
        lbl := text.Label{Face: face, Text: txt}
        dims := lbl.Layout(ops, list.Constraints())


Input queue and handler keys

// Queue maps an event handler key to the events
// available to the handler.
type Queue interface {
    Events(k Key) []Event
// Key is the stable identifier for an event handler.
// For a handler h, the key is typically &h.
type Key interface{}

Pointer event handling

func (b *Button) Layout(queue input.Queue, ops *ui.Ops) {
    for _, e := range queue.Events(b) {
        if e, ok := e.(pointer.Event); ok {
            switch e.Type {
            case pointer.Press:
                b.pressed = true
            case pointer.Release:
                b.pressed = false
    col := color.RGBA{A: 0xff, R: 0xff}
    if b.pressed {
        col = color.RGBA{A: 0xff, G: 0xff}
        Size: image.Point{X: 500, Y: 500},
    pointer.HandlerOp{Key: b}.Add(ops)
    drawSquare(ops, col)

Takes all available events, updates it’s own state, system can know whether the events belong to this button or not is you register the area rectangle arc with a handler.

Window input queue

The Window’s Queue method returns an input.Queue for OS events.

package app // import
func (w *Window) Queue() *Queue


import “”
import “”
import “”

Detect clicks

var queue input.Queue
var c gesture.Click
for _, event := range c.Events(queue) {
    // event is a gesture.ClickEvent, not a raw pointer.Event.

Determine scroll distance from mouse wheel or touch drag/fling

var cfg ui.Config
var s gesture.Scroll
distance := s.Scroll(cfg, queue, gesture.Vertical)


Widgets - the Editor

Complete implementation of a text area field. It’s a complicated widget, but is simple to use. You have to keep state somewhere, but you give it font and font size. Simply call the layout methods and

Initialize the editor

import “”
    var faces measure.Faces
    editor := &text.Editor{
        Face: faces.For(regular, ui.Sp(52)),
    editor.SetText(“Hello, Gophercon! Edit me.”)

Draw, layout and handle input in one call.

editor.Layout(cfg, queue, ops, cs)

Why Gio?

Gio is:

  • Simple. Immediate mode design, no hidden state.
  • Portable. The core of Gio is all Go.
  • Fast. GPU accelerated, very little per-frame garbage.
  • Convenient. Develop on desktop, deploy on mobile.
  • Public domain source (UNLICENCE). Dual licenced MIT to please your lawyers. Most importantly, Gio needs your help to succeed!

I want to bring Go from a place where GUI programming is a fringe activity to a state where it’s normal to use. Maybe in the future we can bring it to a place where you will choose Go for your GUI programming even if you aren’t interested in Go as a programming language, but because the tooling is so good.

Get Cody, the AI coding assistant

Cody makes it easy to write, fix, and maintain code.