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#
| Tool | Version | Needed for |
|---|---|---|
| Go | 1.27.2 or newer | Everything. Vukem's go.mod declares go 1.27.2 |
| A terminal | any | Running go and vukem commands: Terminal on macOS, any shell on Linux, Windows Terminal (PowerShell) on Windows |
| A code editor | any | Writing code. VS Code with the Go extension or GoLand understand Go out of the box |
| Git | any | Optional. When git is on your PATH, vukem new runs git init in the new project |
| Docker | any | Optional. To run PostgreSQL, Redis or MongoDB locally and to build the production image |
| Node.js or Bun | a current LTS | Only 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:
go versiongo version go1.27.2 windows/amd64Any 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 GOPATHThe folder where Go keeps downloaded modules and installed programs. By default it is the go folder in your home directory.. Print it:
go env GOPATHThat 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).
mkdir primer
cd primer
go mod init example.com/primerpackage main
import (
"fmt"
"strings"
)
func main() {
fmt.Println(strings.ToUpper("hello, gopher"))
}go run .HELLO, GOPHERimportbrings in other packages.fmtandstringscome from the standard library; Vukem's packages are imported by their full path, such asgithub.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 calledweb.Ctxandorm.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.
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)
}
}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.Errorfwith%wwraps an error: it adds context and keeps the original inside.errors.Islooks inside wrapped errors for a specific one, called a sentinel errorA package-level error value that callers compare against with errors.Is, such as orm.ErrNotFound..
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 receiverThe value a method is called on. In func (t Task) Label(), t is the receiver..
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())
}[ ] Learn GoFields you leave out get their zero valueThe value a variable has before you assign one: 0 for numbers, false for bool, an empty string for string, nil for pointers, slices, maps and interfaces.: 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:
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.
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{})
}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 pointerA value that holds the memory address of another value. *Task is a pointer to a Task; &t takes the address of t. 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".
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)
}after finishCopy: false
after finish: trueIn 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.
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))
}context deadline exceeded- By convention the context is the first parameter and is called
ctx. defer cancel()runscancelwhenmainreturns, 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.
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")
}worker 3 done
worker 1 done
worker 2 done
all doneThe 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 runnerA long-running part of the app, such as the HTTP server, the queue worker or the scheduler. 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.
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))
}{"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".
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)
}7 true
"" falseIn 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 docs | It means |
|---|---|
package controllers | This file belongs to the controllers package (its folder) |
func (tc *TaskController) Index(c *web.Ctx) error | A 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 struct | Embedding: the struct gets ID, CreatedAt, UpdatedAt |
`json:"title" form:"title"` | Struct tags that name the field in JSON and forms |
&task / *Task | The address of task / a pointer to a Task |
c.Context(), ctx context.Context | The 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.
// 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:
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.