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!