Skip to content

Getting started

Prerequisites

The tools you install before using Vukem, and a 15-minute primer on the Go concepts the rest of the docs rely on.

Before you create your first Vukem app, you need a few tools on your computer and a little bit of Go. This page lists the tools, then teaches the Go you need to read every other page, one small program at a time.

You will learn:

  • which programs to install, and which ones are optional
  • how Go code is organised into packages and modules
  • how Go functions report errors, and why Vukem handlers return error
  • what structs, methods, interfaces and pointers are
  • what context.Context, goroutines, struct tags and generics do in a Vukem app

What you need#

ToolVersionNeeded for
Go1.27.2 or newerEverything. Vukem's go.mod declares go 1.27.2
A terminalanyRunning go and vukem commands: Terminal on macOS, any shell on Linux, Windows Terminal (PowerShell) on Windows
A code editoranyWriting code. VS Code with the Go extension or GoLand understand Go out of the box
GitanyOptional. When git is on your PATH, vukem new runs git init in the new project
DockeranyOptional. To run PostgreSQL, Redis or MongoDB locally and to build the production image
Node.js or Buna current LTSOnly for the Vite + Tailwind build of the web starters and for Inertia. Not needed by go test, and never by the --api starter

Vukem runs on Windows 11/10, Linux and macOS. The default database, SQLite, uses the pure-Go driver modernc.org/sqlite, so you do not need a C compiler.

Install Go

Download the installer for your system from go.dev/dl and follow the installation guide. Then open a new terminal and check the version:

Terminal
go version
Text
go version go1.27.2 windows/amd64

Any version from go1.27.2 up works. The last part (windows/amd64, linux/arm64, darwin/arm64…) is your system and processor.

Check where Go installs programs

go install puts programs in the bin folder of your GOPATH. Print it:

Terminal
go env GOPATH

That folder plus /bin (for example C:\Users\you\go\bin or /home/you/go/bin) must be on your PATH, or your terminal will not find vukem after you install it. The Go installer usually adds it for you.

Optional: Node.js or Bun

Install Node.js (it brings npm) or Bun only if you plan to use a web starter's Vite build or Inertia. You may add it later: a starter's Go tests pass without it.

Go in 15 minutes#

The docs explain every Vukem feature from the ground up, but they assume you can read basic Go. This primer covers exactly the Go concepts the docs use. Each section has a complete program: save it as main.go in an empty folder and run it with go run . (after go mod init example.com/primer, see the first section).

For a longer, interactive introduction, take A Tour of Go (in your browser, nothing to install). For the style that experienced Go programmers follow, read Effective Go.

Packages and modules#

Go code lives in packages. A package is a folder of .go files that all start with the same package line. The package named main is special: it builds a program, and its main function is where the program starts.

A module is a whole project: a folder with a go.mod file that names the module and lists the other modules it depends on. Your Vukem app is one module, and Vukem itself is another (github.com/AlexGalhardo/vukem).

Terminal
mkdir primer
cd primer
go mod init example.com/primer
main.go
package main

import (
	"fmt"
	"strings"
)

func main() {
	fmt.Println(strings.ToUpper("hello, gopher"))
}
Terminal
go run .
Text
HELLO, GOPHER
  • import brings in other packages. fmt and strings come from the standard library; Vukem's packages are imported by their full path, such as github.com/AlexGalhardo/vukem/web.
  • You use an imported name with its package prefix: strings.ToUpper.
  • A name that starts with a capital letter (ToUpper, Println) is exported: other packages may use it. A lowercase name is private to its package. That is why Vukem types are called web.Ctx and orm.Model.

Learn more: How to write Go code, Tutorial: create a module.

Functions and errors#

Go functions may return several values. By convention, a function that can fail returns its result and an error as the last value. When the error is nil (Go's "nothing"), everything went well. You check it right away with if err != nil.

main.go
package main

import (
	"errors"
	"fmt"
	"strconv"
)

var ErrTooYoung = errors.New("too young")

// parseAge returns two values: the age and an error.
func parseAge(s string) (int, error) {
	age, err := strconv.Atoi(s)
	if err != nil {
		return 0, fmt.Errorf("parse age %q: %w", s, err)
	}

	if age < 10 {
		return 0, ErrTooYoung
	}

	return age, nil
}

func main() {
	for _, in := range []string{"12", "abc", "7"} {
		age, err := parseAge(in)
		if errors.Is(err, ErrTooYoung) {
			fmt.Println(in, "-> come back later")
			continue
		}

		if err != nil {
			fmt.Println(in, "->", err)
			continue
		}

		fmt.Println(in, "-> age", age)
	}
}
Text
12 -> age 12
abc -> parse age "abc": strconv.Atoi: parsing "abc": invalid syntax
7 -> come back later
  • := declares a variable and gives it a value in one step; Go works out the type.
  • fmt.Errorf with %w wraps an error: it adds context and keeps the original inside.
  • errors.Is looks inside wrapped errors for a specific one, called a sentinel error.

In Vukem: every handler has the shape func(c *web.Ctx) error. Instead of writing the error page yourself, you return err, and Vukem's central error handler picks the response: orm.ErrNotFound becomes 404, a validation error becomes 422 or a redirect back with the messages. See Error handling.

Learn more: Errors in the Tour, Working with errors in Go 1.13.

Structs and methods#

A struct groups named fields into one type, like a form with labelled boxes. A method is a function attached to a type; the part in parentheses before the name, (t Task), is the receiver.

main.go
package main

import "fmt"

type Task struct {
	Title string
	Done  bool
}

// Label is a method: a function attached to Task.
func (t Task) Label() string {
	if t.Done {
		return "[x] " + t.Title
	}

	return "[ ] " + t.Title
}

func main() {
	t := Task{Title: "Learn Go"}
	fmt.Println(t.Label())
}
Text
[ ] Learn Go

Fields you leave out get their zero value: here Done is false.

A struct may embed another type by writing only its name. The embedded fields then belong to the outer struct too. Vukem models embed orm.Model, which brings the ID, CreatedAt and UpdatedAt fields:

app/models/task.go
type Task struct {
	orm.Model

	Title string `json:"title"`
	Done  bool   `json:"done"`
}

In Vukem: controllers are structs, and their actions are methods: func (tc *TaskController) Index(c *web.Ctx) error.

Learn more: Structs in the Tour, Methods in the Tour, Embedding.

Interfaces#

An interface lists methods. Any type that has those methods satisfies the interface automatically; you never write "implements". Interfaces let a function accept many different types, as long as they can do the same job.

main.go
package main

import "fmt"

// Greeter is any type that has a Greet method.
type Greeter interface {
	Greet() string
}

type English struct{}

func (English) Greet() string { return "Hello" }

type Portuguese struct{}

func (Portuguese) Greet() string { return "Olá" }

func welcome(g Greeter) {
	fmt.Println(g.Greet() + ", gopher!")
}

func main() {
	welcome(English{})
	welcome(Portuguese{})
}
Text
Hello, gopher!
Olá, gopher!

In Vukem: error itself is an interface (any type with an Error() string method). A provider is any type with a Register(app *vukem.App) error method, and r.Resource registers only the actions your controller has: a controller without a Show method gets no show route. The standard http.Handler interface is why any net/http code works inside Vukem.

Learn more: Interfaces in the Tour, Interfaces in Effective Go.

Pointers#

Go passes values by copying them. A pointer lets a function change the caller's value instead of a copy. *Task means "pointer to a Task", and &t means "the address of t".

main.go
package main

import "fmt"

type Task struct {
	Title string
	Done  bool
}

// finishCopy gets a copy: the caller's task does not change.
func finishCopy(t Task) { t.Done = true }

// finish gets a pointer (*Task): it changes the caller's task.
func finish(t *Task) { t.Done = true }

func main() {
	t := Task{Title: "Learn pointers"}

	finishCopy(t)
	fmt.Println("after finishCopy:", t.Done)

	finish(&t)
	fmt.Println("after finish:", t.Done)
}
Text
after finishCopy: false
after finish: true

In Vukem: you pass &task to orm.Create(ctx, db, &task) so the ORM can fill in the new ID and timestamps. Handlers receive *web.Ctx, a pointer, because every helper works on the same request. A pointer that points nowhere is nil; using it panics, which is why Vukem functions check their inputs and return errors.

Learn more: Pointers in the Tour.

context.Context#

A context.Context travels with a piece of work and carries its deadline and cancellation signal. When a browser closes the tab, or the server is shutting down, the context is cancelled, and well-behaved code stops early instead of wasting time.

main.go
package main

import (
	"context"
	"fmt"
	"time"
)

// slowWork stops early when ctx is cancelled.
func slowWork(ctx context.Context) error {
	select {
	case <-time.After(2 * time.Second):
		return nil
	case <-ctx.Done():
		return ctx.Err()
	}
}

func main() {
	ctx, cancel := context.WithTimeout(
		context.Background(),
		100*time.Millisecond,
	)
	defer cancel()

	fmt.Println(slowWork(ctx))
}
Text
context deadline exceeded
  • By convention the context is the first parameter and is called ctx.
  • defer cancel() runs cancel when main returns, so resources are released.

In Vukem: c.Context() returns the request's context. Pass it to every database call (orm.QueryOf[models.Task](db).Get(c.Context())) so a query stops when the client goes away. Providers, runners and commands receive a ctx too. Vukem's request package is called web, not context, so it never clashes with ctx context.Context.

Learn more: Go Concurrency Patterns: Context.

Goroutines, briefly#

A goroutine is a function running at the same time as the rest of the program. You start one with the go keyword, or with sync.WaitGroup.Go, which also lets you wait for it.

main.go
package main

import (
	"fmt"
	"sync"
)

func main() {
	var wg sync.WaitGroup

	for i := 1; i <= 3; i++ {
		// wg.Go starts a goroutine and tracks it.
		wg.Go(func() {
			fmt.Println("worker", i, "done")
		})
	}

	// Wait blocks until every goroutine has finished.
	wg.Wait()
	fmt.Println("all done")
}
Text
worker 3 done
worker 1 done
worker 2 done
all done

The three worker lines may come out in any order: goroutines run independently.

In Vukem: you rarely start goroutines yourself. Go's HTTP server already runs each request in its own goroutine, and app.Run starts each runner in its own. Because many requests run at once, never keep per-request data in a shared variable; keep it in the request (c) or in the database.

Learn more: Concurrency in the Tour, Share memory by communicating.

Struct tags#

A struct tag is a small string after a field, between backquotes. Go ignores it, but packages read it with reflect to learn how to treat the field.

main.go
package main

import (
	"encoding/json"
	"fmt"
)

type Task struct {
	Title string `json:"title"`
	Done  bool   `json:"done"`
	notes string // unexported: encoding/json skips it
}

func main() {
	out, err := json.Marshal(Task{Title: "Read the tour"})
	if err != nil {
		panic(err)
	}

	fmt.Println(string(out))
}
Text
{"title":"Read the tour","done":false}

In Vukem: tags appear everywhere. json:"title" names the field in JSON, form:"title" names it in an HTML form, and env:"MAIL_HOST" default:"localhost" tells config.Load which environment variable fills it (Configuration).

Learn more: encoding/json, Struct types in the spec.

Generics, briefly#

Generics let one function or type work with many types. The type goes in square brackets: First[T any] means "for any type T".

main.go
package main

import "fmt"

// First returns the first item of any slice, and false when empty.
func First[T any](items []T) (T, bool) {
	var zero T
	if len(items) == 0 {
		return zero, false
	}

	return items[0], true
}

func main() {
	n, ok := First([]int{7, 8, 9})
	fmt.Println(n, ok)

	s, ok := First([]string{})
	fmt.Printf("%q %v\n", s, ok)
}
Text
7 true
"" false

In Vukem: generics give you typed results without casts. orm.QueryOf[models.Task](db) builds a query whose Get returns []models.Task, web.Parse(c, requests.TaskSchema) returns a requests.Task, and config.Load[Mail](src) returns a Mail.

Learn more: Tutorial: getting started with generics, Generics in the Tour.

Cheat sheet#

You see in the docsIt means
package controllersThis file belongs to the controllers package (its folder)
func (tc *TaskController) Index(c *web.Ctx) errorA method on *TaskController that takes a *web.Ctx and may fail
if err != nil { return err }Stop and hand the error to the caller (in a handler: to Vukem)
orm.Model inside a structEmbedding: the struct gets ID, CreatedAt, UpdatedAt
`json:"title" form:"title"`Struct tags that name the field in JSON and forms
&task / *TaskThe address of task / a pointer to a Task
c.Context(), ctx context.ContextThe request's deadline and cancellation signal
orm.QueryOf[models.Task]A generic call: the query returns models.Task values

Common mistakes#

Ignoring an error. Go lets you drop a returned error with _, but then failures disappear silently.

Go
// Wrong: if the insert fails, nobody finds out.
_ = orm.Create(c.Context(), db, &task)

// Right: return it, and Vukem renders a 500 and logs the cause.
if err := orm.Create(c.Context(), db, &task); err != nil {
	return err
}

Passing a value where a pointer is needed. orm.Create must write the new ID back into your struct, so it needs its address. Passing task instead of &task does not compile:

Text
in call to orm.Create, type models.Task of task does not match *T (cannot infer T)

An unused import or variable. Go refuses to compile code that imports a package or declares a variable it never uses: "strings" imported and not used. Delete the line, or use the name.

Next steps#