Fullstack v2: let's write UI in Go

Fullstack v2: let's write UI in Go

This is a text version of my presentation at GolangConf ‘24. CFP for the upcoming one is here, Your participation is highly encouraged!

My name is Ilya Glukhov.? I have been writing in Go for the last 7 years. I love this language, and I also like to ask myself some strange, at the first sight, questions. For example: how do different interesting things we write in Go interact with the user? In a classic backend we use RPC (Remote Procedure Call), HTTP protocol or different queues to modify the behaviour of our program.

What about the graphic UI? Doesn't it come from the frontend? Or do we just think so? Let's create a user interface (UI) in Go. The choice of solutions is varied: Gopherjs, gomobile, Qt wrappers, GTK and many more. But if we want to achieve cross-platform compatibility for browsers, mobile devices and desktops, we need a universal UI. Let's take a practical example of how to create it in Go.

Why do you need UI?

Each application may respond to user input in different ways. Depending on the type of application, these may be:

Command line parameters: if you are writing a shell utility.

Environment variables: if you are developing a 12-factor application.

RPC, HTTP or message queues: if it's a backend.

But when it comes to UI, the only thing that comes to mind for many of us is the frontend world....

Programs originally appeared as ‘thin clients’ to mainframes. Personal computers came after. There, logic, business logic, and presentation layer were all implemented, let's say, in one monolith. When the Internet and the concept of hypertext appeared, developers came up with a bright idea - to use a ‘thick client’ on which the presentation of the program would be executed. The Internet back then was slow, processors were weak, and browsers were young, so there were no other options. At first, the concept of Document Object Model (DOM) worked only for the execution of the program representation in the client's browser, but then it started to be used to improve websites. Snowflakes, music, and other non-standard uses of the DOM that underlie hypertext appeared. It took a special effort to implement this in the client browser. So frontend became a separate speciality.

Then came mobile devices with their own operating system, physical representation, a screen that you have to ‘tap’ - and everything became even more complicated.

Advantages and disadvantages

On one hand, sub-specialisation implies that the frontend specialist is well versed and can do some very strange things with the tools they have.

On the other hand, we get the disadvantages:

Different stats and data in different parts of the same application. For example, there may be one list of users on the frontend and a completely different list on the backend.

Two centres of responsibility - frontenders fall on backenders, and backenders fall back on them.

No unified development concept - frontend is mostly about DOM, and backend is more about abstractions and data.

Two or more languages - their own pipeline, their own differences and chaos.

Taking into account all of the above, the logical solution is something multi-purposed like a swiss army knife. That’s where they invented fullstack.

Fullstack v1.

Since JavaScript performed well on the frontend, they decided to extend it to the backend. That's how NodeJS and the first fullstack appeared in 2009. Something similar is happening now. Kotlin became popular in official Android development, and now it is taking share from Java on the backend. And thanks to Kotlin’s cross-platforms capabilities, it has started compiling to different device platforms. In other words, we have fullstack emerging from the frontend again. Large corporations are not left out of this story.

Cross-platform solutions for full-stack development

Microsoft bought Xamarin and incorporated it into the .NET platform to enable multi-language development on all known systems.

Meta (then known as Facebook) wondered if the React framework worked well with Web APIs and would work with native APIs. That's how React Native came into being.

Google also made a universal solution - the Flutter framework.

But let's go back to Go and see what we have for this language.

A place of Go in full-stack solutions

There are several modern approaches that involve Go in front-end development.

Svelte (JS) + Go templating. Svelte is the #3 framework on the list of most popular JavaScript frameworks for 2023. Uses Go templating to dynamically recreate pages.

HTMX (JS) + Go (Templ). The #2 framework from the same list is HTMX. It's an attempt to breathe new life into good old HTML, make it reactive and add new features. HTMX works in conjunction with Go (Templ).

Go file server. It's like a template, but with the ability to embed it directly into Go code and use it inside Go files.

http.FileServer(http.Dir(directoryPath))        

What if it is only Go?

After all, we can use just Go. Make a file server out of the box, throw some html files in there, and we'll have some frontend. Sure, it will look like something from the 90s. But to create something more modern, we will need Go cross-compilation and GUI frameworks. Let's get to them

Go cross-compilation

We have different build targets:

wasm (web): GOOS=js GOARCH=wasm (WASI from 1.21)

Version 1.15 has wasm, and since 1.21 we even have access to WASI via wasm.

native code (Android IOS): GOOS=android/ios

It is also possible to build into native code, i.e. targeting for iOS and for Android.

binary (desktop)

Ability to build in Darwin, Windows and Linux.

GUI frameworks

There are quite a few of them. They are listed in this excellent list. For our story it's important to know which framework it all started with, and which ones followed suit:

gomobile - barely supported at the moment, but it was the one that kick-started mobile development for Go.

qt - Qt binding for Go (support for Windows / macOS / Linux / Android / iOS / Sailfish OS / Raspberry Pi) - also an ancient framework, mostly used in C++. Many programmes are written on it. For example, Telegram, KDE and VLC. And binding Qt for Go allows you to develop for almost any system.

go-gtk GTK aka GIMP Toolkit is also an old one. Cinnamon and a bunch of Gnome utilities are made using this tool.

go-sciter HTML CSS for desktops - for writing desktop applications if you are very good at HTML and CSS.

fyne - modern cross-platform framework for Linux, macOS, Windows, BSD, iOS and Android. It is based on Material Design. There is additional logic for creating applications. Very friendly and active community with channels in Slack and Discord, FyneConf, books, videos.

GioUI is a more ‘old-school’ cross-platform framework: Linux, macOS, Windows, Android, iOS, FreeBSD, OpenBSD and WebAssembly. Positioned as true open-source. Specifically, they have a mirror on GitHub, but they are based specifically on sourcehut and use a mailing list for discussion. Among the distinguishing features are their simple structure and the ability to commit even for those who are banned on GitHub.

I chose GioUI for myself. I liked their true open source nature and their feature set. It has some extra logic, but in the end it's mostly just a library. This gives it a simpler structure in my opinion.

GioUI uses Immediate graphic mode. And this point should probably be further explained.

Almost all graphic software uses one of the two approaches:

  1. Retained Mode (examples: JavaScript, Document Object Model, PDF files)

First, a tree or a structure containing elements is created. Then we assign parameters in the application layer, and based on them, the framework draws a graphical scene using hardware.

2. Immediate mode, when we directly draw the scene in the application layer.

The library uses native code for the respective hardware where our code is running. GioUI uses exactly this graphical approach.

At this point I think that's enough for the theory, it's time to move on to practice. More precisely, we’re going to create an application.

Creating an application

Suppose we decided to make Trader AI - a trader console for mobile, web and desktop. This application will give the user advice using artificial intelligence.

Problem Statement

This app will let the user:

  • view quotes,
  • edit the list of quotes,
  • create limit orders, i.e. send them to the exchange or execute them,
  • monitor the status of their portfolio,
  • ask questions to the AI-assistant.

Tools

GioUI can be used on three layers when creating an application:

  1. Standard elements: widgets,
  2. Graphical elements, if you need special graphics: canvas and draw,
  3. Native API (web API) - additional features to access video playback, file operations and anything else that requires access to the underlying system.

Widget

We’re going to start with a widget. This is an abstraction that will describe an element of a graphical scene. It can be created and drawn. It can also handle user input.

It is important that we, as developers, have the ability to reach the widget and get its state if the user interacts with it in some way.

The widget should also have a Layout.

In GioUI terms, a widget is a function.

func(gtx layout.Context) layout.Dimensions        

If you want something to be a widget, you must describe it as a function with a given contract: a graphical context as an input parameter and a position on the graphical scene as an output parameter.

Material Design

GioUI uses Material Design, an open-source design system developed by Google. You need it to avoid getting bogged down in the details of a graphical representation. For example, you don't have to think about how to make a button. Thanks to Google, you don't have to waste time on that. Most applications on Android use Material Design.

This system describes the following elements:

  • Buttons,
  • Icon buttons,
  • Radio buttons,
  • Labels,
  • Switches,
  • Progress bars.

These are just a few basic elements to make the idea clear.

Elements

We will have in our application:

  • A button - a basic way to interact with the user.
  • An input field, for example, the quantity we want to sell, the ticker we want to add. This is text.
  • A grid - some kind of dynamic view filled with tickers, where we can add or remove tickers.
  • A table to describe the values we have and the changes that happen to them.
  • A text output field or logger where communication with the AI assistant will take place.

Flex

This is a horizontal and vertical grouping of graphic elements. A flex element can be either rigid, i.e. fixed, or flexible, i.e. adjustable to its neighbours.

Let's go directly to the code.

Basic design

This Hello, World is a programme that will display a blank screen with close-open buttons and a title.


The system halts, waiting for an exit command, or some event that will end execution.

Design without design

Since we are doing frontend, we need a design. But I am a backend developer, so I made a simple design, even without Figma:

?We will have five parts of the app: order management, ticker management, tickers, portfolio and AI advisor.

Elements

To render the button, let's use the Material Designer package.

Button

Let's take the button function and pass the graphic theme to it, as well as a receiver that can be any object that will hold the state of the button. We also pass the name of the button and return the Layout position.


This will look much scarier in our application.

This will be framed in the Rigid Flex element. I added a black border to beautify.

Radio button

Roughly the same logic applies. We use Rigid Flex again, a radio button function from the Material Design package. We pass to it a theme, a receiver object, receiver state, and presentation name for that state.


Important point: the receiver is the same for both positions. This is because the receiver is an enum, i.e. an enum that contains a possible value from the ones we expect. By reading this value, we get what state is currently selected.

The radio button in our application will look like this.

This is again wrapped in flex for the good packaging.

Input field

The simplest thing is the input field. In Editor we pass the theme, a receiver and a name.

That looks scarier in the app.

To be honest, I made it look neater. I manually set the background, border, font, and packed everything in Rigid again.

I used this approach to showcase how to control the view directly from the code, and change the background, border, and font depending on some events.

Main

This is no longer a Hello World, but here is our function.

It looks pretty similar to the previous example, but there is an important difference - a “go update” is run at the beginning of the main function. This is a handler that interacts with the outside world. In our case, it is a stock exchange. It uses some API to somehow listen to the exchange, or the exchange itself sends something for us.

Next, we already know how to create a new window with the necessary dimensions. We run a handler, that is, a procedure that will handle the state of our window.

And we stop at app.Main waiting for the completion event.

Event handler

Now let's see what happens inside the event handler.

We read the message and put it into the message channel, which is then read to the handlers. Next, we stop at the latch waiting for processing to happen.

This is because the graphics scene is both the source of the events and the representation we are rendering. We have a livelock, that is, a conflict of interest between the two parties. That's why we wait for processing and only then try to listen to what we have going on.

Graphic handler

An important point: We have two ways of rendering the graphic scene.

  1. If some business logic event was initiated by the user (for example an order creation was started), we recreate the scene.
  2. If any external event comes to us, such as a ticker update, we update the existing scene data and redraw the existing scene.


If we receive a frame control event, the processing is done by a switch with a type.


If the message is relevant to us, we construct a page. If it's a closing message, we close the app with a graceful shutdown.

If we get a relevant message, we construct a new graphical context that contains all the information about what is happening at the graphic scene, and call the ‘draw top screen’ function.

It's the only screen we have, so it's in charge of rendering. We pass it the graphical context and the Material Design theme we created elsewhere. Next, we call the Frame function on the received event to redraw the scene.

In case we get a data update signal, we find the ticker by name in our application and update the price value and price change. After that we do Invalidate to trigger the redraw of the scene.


That's pretty much all the logic that's inside the app.

Deploy

First, let's see how the desktop deployment is done.

Desktop

This is a common go run or go build. It could be cross-compiling from Linux to Windows, from Windows to Linux. The only particular case is that it won't happen that easily with iOS. You have to have Xcode installed on iOS in order to compile this. If you develop on a platform other than Mac, depending on where you host the code you have a few options:

  • Docker;
  • GithubActions;
  • GitlabRunner.

I used GitHub, which has so called “actions”. I used these to create a pipeline. With this build, even though I don't have a Mac, I can make sure it builds.

On Linux, our entire application is a few files. We do a go run and everything runs.


We have a portfolio on the screen with some funny coins like dodge. We have a watch list and are watching some tickers. There are buttons at the top, and they are not active because we don't have enough info to activate them yet. To activate the buttons, we have to, for example, select a ticker and sell, for example, USDT.

We also have an AI assistant. We can ask them about something and get an answer.

Mobile

For Android, we will need to create an APK. It is an archive with manifest, binary code and an entry point for the Java machine. So we need the Android SDK and NDK bundle installed.

We can call gogio, it's a special utility from GioUI with Android target, which will build the binary code and put it into an APK file named “trader”. This will result in two new files: a signature file to put somewhere and an .apk file.

gogio  -target android        

Now all we have to do is to install the resulting APK on an Android device and launch it.

When launched, our Trader application appears on the screen. We see exactly the same interface that we generated from the same code, no additional actions. But if we enter a quantity, the Google keyboard appears.

On iOS, like previously mentioned, you need additional steps, mostly related to the developer signature.

Web

To build our application, we run gogio with “js” target.

gogio  -target js        

As a result, we get a file server, which we will run with the help of the goexec utility. It allows you to run oneliners without building them. That is, on your server you can just do “goexec ListenAndServe” and the server will start.

https://github.com/shurcooL/goexec

goexec 'http.ListenAndServe(":8080",http.FileServer(http.Dir("server")))'        

As a result, we get a file server, which we will run with the help of the goexec utility. It allows you to run oneliners without building them. That is, on your server you can just do “goexec ListenAndServe” and the server will start.

As a result of the build we have a new server directory with three files:

  • main.wasm - this is just the wasm code for our application.
  • wasm.js - the entry point, just a transport, that is JavaScript that takes wasm and puts it in the browser wasm machine.
  • index.html, which simply runs wasm.js if accessed.

If we start our server with goexec and go to localhost:8080, our application will run in the browser.

Conclusion

I would like to draw your attention to the fact that we used only Go-code. It's quite small, so we managed to build everything literally in a few clicks for mobile, web and desktop. And I think we have achieved our goal - to make a graphical UI using only Go.


You can take a closer look for the code resulted at https://github.com/habuvo/gio-simple-app

#golang #goconf #ui



Egor Glukhov

Senior Android Developer | 10+ years

2 个月

Widget as a function and no classes. Add some state management and we'll have Jetpack Compose in Go. :)

要查看或添加评论,请登录

社区洞察

其他会员也浏览了