Code and Life

Programming, electronics and other cool tech stuff

Supported by

Supported by Picotech

Combine Go (golang) and SvelteKit for GUI

This post outlines the basics of creating a project that combines Go (or "golang" as googling for "go" is a pain — why didn't the guys at Google think of this?) native backend serving a web UI / GUI running on SvelteKit.

In a nutshell, this involves creating a new go project, creating a simple web server program that supports serving files from a static folder, and finally creating a SvelteKit project and configuring it to produce static content into that folder. But let's do a short detour on why this might be useful!

Combining native executable with Web UI

Native graphical user interfaces are not easy on any platform, and after looking at Qt, WxWidgets, Electron etc. I decided all had either major shortcomings, huge learning curves or resulted in way too large packages.

Doing a native web server, on the other hand, is quite easy using Go. I also investigated C and C++, but at least on Windows you very quickly run into MinGW vs. Visual Studio issues, runtimes, build systems and all that chaos, whereas Go pretty much produces executables with minimum fuss.

Once you have a web server, you can just serve a web UI and the user can run the executable and open the UI in their browser.

Simple web server with Go

Once you are comfortable creating a "Hello world" level app in Go, making a simple app for web server is not too hard:

$ mkdir project
$ cd project
project$ go mod init example/project

Here's a simple web server you can paste into main.go

package main

import (
    "encoding/json"
    "log"
    "mime"
    "net/http"
)

func databases(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    w.Header().Set("Access-Control-Allow-Origin", "*") // for CORS
    w.WriteHeader(http.StatusOK)
    test := []string{}
    test = append(test, "Hello")
    test = append(test, "World")
    json.NewEncoder(w).Encode(test)
}

func main() {
    // Windows may be missing this
    mime.AddExtensionType(".js", "application/javascript")

    http.Handle("/test", http.HandlerFunc(databases))
    http.Handle("/", http.FileServer(http.Dir("static")))
    log.Fatal(http.ListenAndServe(":8080", nil))
}

This will serve the http://localhost:8080/test URL using the function databases (see net/http docs for details of that module), essentially returning a JSON array ["Hello", "World"], and everything else will check if a file with the given path is found under static subfolder.

Let's make that subfolder and run our server:

project$ mkdir static
project$ cd static
project/static$ echo "This is a static file" > file.txt
project/static$ cd ..
project$ go run .

You can now go to http://localhost:8080/file.txt and see the static file contents, or / to see a list of files in the static folder (note that if you have index.html in your folder, that will be served instead), or /test to get that JSON array. Nice! Note that you can break

Creating a simple SvelteKit app

SvelteKit tutorial outlines the simple steps we'll follow to create a subfolder 'frontend' to contain our "SvelteKit subproject". Another alternative would be a separate directory, but let's be as simple as possible here. Feel free to choose TypeScript and adapt the rest of the tutorial accordingly:

project$ npm init svelte@next frontend
...
✔ Which Svelte app template? › Skeleton project
✔ Use TypeScript? … No
...
project$ cd frontend
project/frontend$ npm install
project/frontend$ npm run dev -- --open

The -- --open part should pop up a browser window at http://localhost:3000 with a simple message. Let's modify that a bit to call our Go server (you need to have go run . on another console running for this to work). Edit fontend/src/routes/index.svelte:

<script>
    import { onMount } from "svelte";

    let test = [];

    onMount(async function() {
        const response = await fetch('http://localhost:8080/test');
        test = await response.json();
    });
</script>
<h1>Combining SvelteKit and Golang server</h1>

<ul>
    {#each test as t}
    <li><a href="/subdir/{t}">{t}</a></li>
    {/each}
</ul>

The code above uses Svelte's onMount function to connect to our Go server to fetch the JSON list of strings and update the value of test to that list. Magic of Svelte then updates the #each loop to render a list of links using the contents of that test variable.

Let's put a dummy file to match those /subdir/Text URLs as well by creating subdir/[str].svelte in the routes subfolder:

<script>
    import { page } from '$app/stores';
</script>

{$page.params.str}

Once you have created this, the development server should automatically rebuild your project and refresh it on the fly, so you should now be able to visit http://localhost:3000/subdir/Hello and see the text Hello displayed. Awesome!

Note: the reason we're bothering with a "slug route" is to make sure the statically built version we're doing next will handle those without issues as well. I was uncertain if dynamic routes would "fall back" to some SvelteKit specific server side rendering and fail to work once the frontend is statically served by a Go server.

This "dual server" setup is nice for development time, as your changes are reflected automatically on SvelteKit side, without need to regenerate and redeploy. But for the final version, we'll need a bit more...

Configuring SvelteKit to produce static HTML+JavaScript

Now a development server is all nice and good, but we'd like the whole SvelteKit frontend being built into the project/static subfolder so the Go application can serve that, and there is no need to have anything else on the user's machine (like Node, npm, Svelte etc.). For this, we'll swap the default SvelteKit adapter to a static one, as discussed here:

https://kit.svelte.dev/docs/adapters

First, we'll install the adapter-static:

project/frontend$ npm i -D @sveltejs/adapter-static@next

Then, reconfigure the svelte.config.js in frontend directory to use it:

import adapter from '@sveltejs/adapter-static';

/** @type {import('@sveltejs/kit').Config} */
const config = {
	kit: {
		adapter: adapter({
			pages: '../static',
			assets: '../static',
		})
	}
};

export default config;

Note that we are setting the target directory of both created pages and other assets to go to ../static, i.e. project/static. Now, building the static version is really easy:

project/frontend$ npm run build
...
project/frontend$ cd ..
project$ go run .

You should now be able to visit http://localhost:8080 and see your freshly built SvelteKit frontend!

Final notes

Wow, that was not too hard! Armed with these tips, you should be able to combine the native speed & capabilities of Go, and provide the end-user with a simple web interface to navigate it. And compared to those 100 MB application sizes, the resulting .exe is in few megabytes (Go is not exactly tiny) and the GUI runs in tens of kilobytes!