DEV Community

Cover image for An introduction to Go, by leveraging your favorite JS flavor.
Aodhan Hamilton
Aodhan Hamilton

Posted on

An introduction to Go, by leveraging your favorite JS flavor.

I'll assume you have a comprehensive understanding of JavaScript and frontend. I'll just focus on the Go code and to familiarize ourselves a bit with Go, we'll create a simple desktop app with Wails to track our subscriptions.

Simply put Wails is like the Tauri of the Go world, they both leverage the native web view of your operating system (WebView 2 in my case as I'm developing on windows) to build desktop apps, but while Tauri uses Rust as the backend logic language, Wails uses Go.

Default Wails boilerplate app

Prerequisites

  • Make sure you have Go (1.18+) on your system
  • Make sure you have Node & NPM on your system

Wails mentions in their docs how you can download, configure and verify these tools on your system.

Install the Wails CLI

Installing the Wails CLI via go install will allow us to call the Wails cli from anywhere in our system

go install github.com/wailsapp/wails/v2/cmd/wails@latest

Generate a Wails app

Generate a wails app with React and TypeScript, via the CLI
(or with your favorite flavor. Svelte, Vue, Lit, Preact or Vanilla JS)

wails init -n myproject -t react-ts

or without TypeScript if you prefer

wails init -n myproject -t react

You can run your application in development mode by running wails dev from your project directory.

Typically package main and usually main.go are the entry point of any go program and we'll rarely need to touch this file, but this will create and run our app.
main.go

package main

import (
    "embed"
    "github.com/wailsapp/wails/v2"
    "github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
)

//go:embed all:frontend/dist
var assets embed.FS

func main() {
    // Create an instance of the app structure
    app := NewApp()

    // Create application with options
    err := wails.Run(&options.App{
        Title:  "Simple Suscription Tracker",
        Width:  1080,
        Height: 710,
        AssetServer: &assetserver.Options{
            Assets: assets,
        },
        BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
        OnStartup:        app.startup,
        Bind: []interface{}{
            app,
        },
    })

    if err != nil {
        println("Error:", err.Error())
    }
}

Enter fullscreen mode Exit fullscreen mode

OnStartup calls a method called startup on our app instance, which is found in app.go

We define a method by using parens after the funckeyword and before the function name, passing in a the thing itself with a pointer type, which references the struct in memory.

In this case, the startup method is already defined for us

// startup is called when the app starts. The context is saved
// so we can call the runtime methods
func (a *App) startup(ctx context.Context) {
    a.ctx = ctx
}
Enter fullscreen mode Exit fullscreen mode

Final version of complete introduction demo wails app

//NOTE: CONNECTING TO A DB FROM A DESTOP APP IS NOT BEST PRACTICES AND NOT RECOMMENED, THIS IS JUST FOR LEARNING PURPOSES, TO LEARN GORM.

In this project, I'm using gorm to connect to a railway Postgres Database to persist subscriptions, so I'll create a method to connect and create the database tables if they don't already exist.

func (a *App) connectToDB() {
    err := godotenv.Load()
    if err != nil {
        log.Fatal("Error loading .env file")
        return
    }
    dsn := os.Getenv("DATABASE_URL")
    conn, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})

    if err != nil {
        log.Fatal("Error connecting to the db")
        return
    }

    db = conn
    log.Println("Connected To DB")
}

func (a *App) createTables() {
    db.AutoMigrate(&Subscription{})
}
Enter fullscreen mode Exit fullscreen mode

The connectToDB func needs a string to connect to the DB an we don't want to expose it in our code, so we'll utilize gotdotenv to load the env variable from our .env and then add it to our .gitignore.

Install godotenv and gorm

go get github.com/joho/godotenv
go get -u gorm.io/gorm

And if like me, you're using Postgres, you'll need to install the driver for gorm

gorm.io/driver/postgres

You'll notice the connection is assigned to a variable db, that is outside the scope of the function. I defined it globally, so we can use it from other functions and in go, to initialize a variable without a value, we use the varkeyword and give it a type.

var db *gorm.DB
Enter fullscreen mode Exit fullscreen mode

The type here is a pointer to the gorm connection.

Now we can call these two methods on our app on startup by adding them to the startup func.

func (a *App) startup(ctx context.Context) {
    a.ctx = ctx
    a.connectToDB()
    a.createTables()
}
Enter fullscreen mode Exit fullscreen mode

Go has a concept of a struct, a struct defines a custom data structure

type Subscription struct {
    Id        string    `gorm:"primaryKey" json:"Id"`
    Name      string    `json:"Name"`
    Price     string    `json:"Price"`
    CreatedAt time.Time `json:"CreatedAt"` 
// Automatically managed by GORM for creation time
    UpdatedAt time.Time `json:"UpdatedAt"` 
// Automatically managed by GORM for update time
}
Enter fullscreen mode Exit fullscreen mode

The code inside of the backticks in the struct are known as field tags, they are optional and tell certain libraries how to treat each struct field and gorm has many.

gorm:"primaryKey" is pretty self explanatory, but it specifies the column as primary key, gorm automatically uses fields named CreatedAtand UpdatedAtto automatically track the creation and update times of records.

GORM simplifies database interactions by mapping Go structs to database tables and AutoMigrateruns auto migration for given models.

Here, we migrate our subscription struct and pass in an empty struct.

func (a *App) createTables() {
    db.AutoMigrate(&Subscription{})
}
Enter fullscreen mode Exit fullscreen mode

Now we have our DB setup, we'll define some functions to be called from our frontend. You'll notice these functions are capitalized here too, that's to ensure they can be exported from that package.

func (a *App) GetAllRecords() (string, error) {
    var subscriptions []Subscription
    result := db.Find(&subscriptions)

    if result.Error != nil {
        log.Fatalln("Error executing query:", result.Error)
    }

    data, err := json.Marshal(subscriptions)
    if err != nil {
        log.Fatalf("Failed to marshal data: %v", err)
    }

    return string(data), nil
}

func (a *App) CreateRecord(name string, price string) {

    v4, err := uuid.NewRandom()
    if err != nil {
        log.Println("Error creating uuid", err)
        return
    }

    v4Str := v4.String()
    log.Println(name)
    log.Println(price)

    subscription := Subscription{Id: v4Str, Name: name, Price: price}
    result := db.Create(&subscription)

    log.Printf(fmt.Sprint(result.RowsAffected))
}

func (a *App) GenUUID() (string, error) {
    v4, err := uuid.NewRandom()

    if err != nil {
        return "", err
    }

    return v4.String(), nil
}

func (a *App) DeleteById(id string) {
    db.Delete(&Subscription{}, "Id = ?", id)
}
Enter fullscreen mode Exit fullscreen mode

Now we can import and call our functions from our frontend.
frontend\src\App.tsx

import { CreateRecord, GetAllRecords } from "../wailsjs/go/main/App";
Enter fullscreen mode Exit fullscreen mode

If you'd like me to cover what I'm doing in the TS and cover more of the go code, let me know.

The github repo can be found here

Top comments (0)