I - Why develop desktop applications today?
This is a question that all developers have asked themselves, especially if they come from the webdev world: "If I can run almost anything that will render in the browser and serve almost any purpose I want, who would need to download our application and run it on their computer?". But aside from the obvious requirement of the work we are doing (for ourselves or for a company, e.g. being able to make use of all the OS features, better performance, offline capabilities, improved security and integration, etc.), there is the experience that we as developers gain from touching new aspects of programming that will always enrich us.
If you are passionate about Golang, like me, and you have developed backend in this language, but you have also done frontend with HTML, CSS and JavaScript (or some of its frameworks) this post is for you, because without needing to learn a new technology you are more than capable of creating desktop applications.
II - The answer is called Wails
Chances are you already know . Both use web technologies for the frontend; the first one uses JavaScript (or rather, NodeJs) in its backend, and the second one uses Rust. But both have more or less notable drawbacks. Electron apps have very large binaries (because they package an entire Chromium browser) and consume a lot of memory. Tauri apps improve these aspects (among other things, because they use WebView2 [Windows]/WebKit [macOS & Linux] instead of Chromium), but the binary is still relatively large and their compilation times are… are those of Rust 😰 (not to mention their learning curve, although I love Rust, I really mean it 😀).
By using Wails, you get the best of all these worlds of desktop application development with web technologies that I just described, plus all the advantages that come with using Go:
- easy to learn and extraordinarily expressive language,
- fast execution and, above all, fast compilation,
- "out of the box" cross-compilation,
- small binaries that run with moderate memory consumption (as an example, the application we will develop here would be ~100 Mb with Electron and with other native GUI frameworks like Fyne, ~20 Mb; with Wails it is only 4 Mb 😀!!),
- and the possibility to use the web framework of your choice (even Vanilla JS) with which you get the ease of designing "modern" UIs that improve the user experience.
Yes, if I wanted to use Go to create desktop applications there are other possibilities (native or not). I would mention . Fyne is a GUI framework that allows the creation of native apps easily and although they may have an elegant design, the capabilities of the framework are somewhat limited or require a great effort from the developer to achieve the same thing that other tools and/or languages would allow you to do easily. I can say the same about go-gtk, which is a Go binding for GTK: yes, it is true that you will get native applications whose limits will be in your own capabilities, but getting into the GTK library is like going on an expedition through the jungle 😰…
III - An approach to Wails: Nu-i uita - Minimalist password manager
First of all, for those who are wondering what Nu-i uita means: in Romanian it roughly means "don't forget them". I thought it was an original name...
You can see the entire code of the application in this GitHub (for Windows & Linux).
. In any case, it is essential that you install its powerful CLI (go install github.com/wailsapp/wails/v2/cmd/wails@latest), which allows you to generate a scaffolding for the application, hot-reload when editing code, and build executables (including cross-compilation).
The Wails that automates the generation of projects with Svelte5. In this case, you will also need to have the Wails CLI installed.
The features of the application I mentioned above constitute the requirements of any todoapp (which is always a good way to learn something new in programming), but here we add a plus of features (e.g. both in the backend, the use of symmetric encryption, and in the frontend, use of Internationalization) that make it a little more useful and instructive than a simple todoapp.
Ok, enough of the introduction, so let's get down to business 😀.
IV - Wails project structure: an overview of how this framework works
If you choose to create a Wails project with Svelte+Typescript with the CLI by running the command wails init -n myproject -t svelte-ts (or with the bash script I created and that I already told you about before, that generates Wails projects with Svelte5) you will have a directory structure very similar to this one:
.
├── app.go
├── build
│ ├── appicon.png
│ ├── darwin
│ │ ├── Info.dev.plist
│ │ └── Info.plist
│ ├── README.md
│ └── windows
│ ├── icon.ico
│ ├── info.json
│ ├── installer
│ │ ├── project.nsi
│ │ └── wails_tools.nsh
│ └── wails.exe.manifest
├── frontend
│ ├── index.html
│ ├── package.json
│ ├── package.json.md5
│ ├── package-lock.json
│ ├── postcss.config.js
│ ├── README.md
│ ├── src
│ │ ├── App.svelte
│ │ ├── assets
│ │ │ ├── fonts
│ │ │ │ ├── nunito-v16-latin-regular.woff2
│ │ │ │ └── OFL.txt
│ │ │ └── images
│ │ │ └── logo-universal.png
│ │ ├── lib
│ │ │ ├── BackBtn.svelte
│ │ │ ├── BottomActions.svelte
│ │ │ ├── EditActions.svelte
│ │ │ ├── EntriesList.svelte
│ │ │ ├── Language.svelte
│ │ │ ├── popups
│ │ │ │ ├── alert-icons.ts
│ │ │ │ └── popups.ts
│ │ │ ├── ShowPasswordBtn.svelte
│ │ │ └── TopActions.svelte
│ │ ├── locales
│ │ │ ├── en.json
│ │ │ └── es.json
│ │ ├── main.ts
│ │ ├── pages
│ │ │ ├── About.svelte
│ │ │ ├── AddPassword.svelte
│ │ │ ├── Details.svelte
│ │ │ ├── EditPassword.svelte
│ │ │ ├── Home.svelte
│ │ │ ├── Login.svelte
│ │ │ └── Settings.svelte
│ │ ├── style.css
│ │ └── vite-env.d.ts
│ ├── svelte.config.js
│ ├── tailwind.config.js
│ ├── tsconfig.json
│ ├── tsconfig.node.json
│ ├── vite.config.ts
│ └── wailsjs
│ ├── go
│ │ ├── main
│ │ │ ├── App.d.ts
│ │ │ └── App.js
│ │ └── models.ts
│ └── runtime
│ ├── package.json
│ ├── runtime.d.ts
│ └── runtime.js
├── go.mod
├── go.sum
├── internal
│ ├── db
│ │ └── db.go
│ └── models
│ ├── crypto.go
│ ├── master_password.go
│ └── password_entry.go
├── LICENSE
├── main.go
├── Makefile
├── README.md
├── scripts
└── wails.json
What you just saw is the finished application structure. The only difference with the one generated by the Wails CLI is that with it you will get the scaffolding of a Wails application with a Svelte3+TypeScript frontend, and with in general and at the same time particularizing the explanation for our case:
(here on DEV.to) by of a password manager, that he first created with . The most essential are these 3 functions:
/* crypto.go */
func keyfy(password string) []byte {
// this is to pad the password to make it 32
password = fmt.Sprintf("%032s", password)
key := ""
for i := 0; i < 32; i++ {
key += string(password[i])
}
return []byte(key)
}
func encrypt(plaintext string, key []byte) ([]byte, error) {
aes, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(aes)
if err != nil {
return nil, err
}
// We need a 12-byte nonce for GCM (modifiable if you use cipher.NewGCMWithNonceSize())
// A nonce should always be randomly generated for every encryption.
nonce := make([]byte, gcm.NonceSize())
_, err = rand.Read(nonce)
// https://cs.opensource.google/go/go/+/refs/tags/go1.23.4:src/crypto/rand/rand.go;l=26
if err != nil {
return nil, err
}
// ciphertext here is actually nonce+ciphertext
// So that when we decrypt, just knowing the nonce size
// is enough to separate it from the ciphertext.
ciphertext := gcm.Seal(nonce, nonce, []byte(plaintext), nil)
return ciphertext, nil
}
func decrypt(ciphertext []byte, key []byte) (string, error) {
aes, err := aes.NewCipher(key)
if err != nil {
return "", err
}
gcm, err := cipher.NewGCM(aes)
if err != nil {
return "", err
}
// Since we know the ciphertext is actually nonce+ciphertext
// And len(nonce) == NonceSize(). We can separate the two.
// Therefore, we are sure that len(ciphertext) < nonceSize
// will never be true.
nonceSize := gcm.NonceSize()
nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return "", err
}
return string(plaintext), nil
}
I won't go into the details of symmetric encryption with , here on DEV.to.
AES is a block cipher algorithm that takes a fixed-size key and fixed-size plaintext, and returns fixed-size ciphertext. Because the block size of AES is set to 16 bytes, the plaintext must be at least 16 bytes long. Which causes a problem for us since we want to be able to encrypt/decrypt arbitrary sized data. To solve the problem of minimum plaintext block size the block cipher modes exist. Here I use is essentially responsible for encoding/decoding to base64 the input/output of the functions described above.
To store all the application data we use & package because most of its code is related to the way of working with cloverDB, which you can check in its .
See you in the second part. Happy coding 😀!!
SOCIAL SHARE CARD GENERATOR