Your Keep in Go

The program you already wrote, in a language that trusts you more than lux did.

Go is a small, plain language built for programs that run on servers and talk over networks, and it is deliberately unclever — there is usually one obvious way to do a thing and no second way. Coming from lux you will find most of it immediately readable.

But two ideas lux taught you do not survive the trip, and one of them can bite. That is what most of this guide is about. The Swift guide could afford to be mostly about spelling; this one cannot. If you read only one section, read the one about missing values.

Get it in front of you

Converting costs nothing and needs nothing installed:

$ lux convert go world.lux > main.go

As with Swift, the top of the file is a prelude lux wrote to make printing and file handling behave the way they do under lux run. Skip it. Your program starts here:

type World struct {
	room     Room
	items    []string
	doorOpen bool
	playing  bool
}

Two things are already different and both are cosmetic. Go puts the type after the name rather than after a colon, and Go's own formatter lines the names up in a column — every Go program in the world is formatted by the same tool, so arguments about layout do not happen.

To run it you need Go installed, and then:

$ go mod init keep
$ go build -o keep .
$ ./keep

That first line is Go asking what your project is called. It plays exactly as it did under lux run — same rooms, same locked door, same secret — and that is checked on every release.

What comes across unchanged

More than you would expect. Functions, loops, conditions, and strings are all recognisable on sight.

your lux

func has(items: [string], thing: string) -> bool {
    for it in items {
        if it == thing {
            return true
        }
    }
    return false
}

the Go

func has(items []string, thing string) bool {
	for _, it := range items {
		if it == thing {
			return true
		}
	}
	return false
}

Four differences, all spelling. The type goes after the name. An array is []string rather than [string]. The return type has no arrow. And walking a list is for _, it := range items, where that first underscore is the position — Go hands you both the index and the value, and the underscore says you do not want the index.

You will also meet :=, which makes a new name and works out its type for you. It is roughly lux's var, and Go has no let — nothing stops a name being changed later, so that discipline is yours to keep now.

Your enum becomes a shape

Here is the first real change. Go has no enums. Your five rooms come out like this:

type Room interface{ isRoom() }

type RoomEntrance struct{}

func (RoomEntrance) isRoom() {}

type RoomHall struct{}

func (RoomHall) isRoom() {}

Read that slowly, because it is a genuinely different idea and a good one to own. Room is no longer a list of five things. It is an interface — a description of what something must be able to do. Anything with a method called isRoom counts as a Room, and lux has made five empty structs that each have one.

That method does nothing and returns nothing. Its entire job is to be a membership card.

So Room.hall becomes RoomHall{} — making one of those empty structs — and matching on a room becomes asking what type you are holding:

func describe(room Room) string {
	switch room.(type) {
	case RoomEntrance:
		return "You stand at the mouth of an old stone keep…"
	case RoomHall:
		return "A wide hall, its banners long rotted…"
	}
}

That .(type) is called a type switch, and it is Go asking "which of these is it actually?"

Here is the part to take seriously. In lux, leaving out a room was an error — the language counted your arms and told you which one was missing. Go will not. An interface can be satisfied by anything at all, including types you write tomorrow, so there is no closed list to check against. Miss a case and Go compiles your program happily and falls through to whatever you wrote at the bottom.

That safety net was real and you have just lost it. What replaces it is a habit: when you add a room, search the file for the other rooms and visit every place they are named.

Missing values are not checked

The section to read twice

This is the one that can actually hurt you, and it follows from the last section.

In lux, exit hands back an Option<Room> — a room or nothing — and the only way to get the room out is to open it and handle both cases. Swift keeps that promise exactly. Go does not, and the why is better seen than taken on trust.

Because a Go interface can already hold nothing, lux has nowhere to put the Option wrapper. So it drops it, and none becomes nil:

func exit(room Room, dir string) Room {
	switch room.(type) {
	case RoomEntrance:
		switch dir {
		case "north":
			return (RoomHall{})
		default:
			return nil
		}

Look at that return type. It says Room. It does not say "a room or nothing" — there is no way to say that in Go. The possibility of nothing has moved out of the type and into your memory — exactly where lux spent a whole section getting it out of.

The translation still checks, because your lux did:

func walk(w World, dir string) World {
	if rOpt := exit(w.room, dir); rOpt != nil {
		r := rOpt
		return tryEnter(copyWorld(w), r)
	} else {
		return cantGo(copyWorld(w))
	}
}

But nothing makes you. Write your own Go, forget the != nil, and the compiler says nothing:

fmt.Println(describe(exit(RoomHall{}, "west")))   // no complaint

That compiles clean. Even go vet, Go's tool for catching suspicious code, has nothing to say about it. What happens next depends on your program: sometimes a quietly wrong answer, and sometimes this, at the moment a player walks the wrong way:

panic: runtime error: invalid memory address or
       nil pointer dereference
[signal SIGSEGV: segmentation violation]

That is the crash lux was built to make impossible. It is the most common bug in the languages that allow it, and you are now writing in one of them.

Go does have a good version of this for one specific job — looking something up in a dictionary — and it is the shape Go prefers, so learn it early:

exits := map[string]Room{"north": RoomHall{}}

if next, ok := exits["north"]; ok {
	fmt.Println(describe(next))
}

That ok is a second value saying whether the thing was there. It is the same idea as Option, handled by convention rather than enforced by the compiler — a promise between programmers rather than a rule.

Failure is a second value

lux's Result — the thing readFile and writeFile hand back — becomes Go's most distinctive habit. A function that can fail returns two things: what you asked for, and what went wrong.

func writeFile(path string, contents string) error {
	if err := os.WriteFile(path, []byte(contents), 0644); err != nil {
		return fmt.Errorf("could not write %s: %s", path, ioReason(err))
	}
	return nil
}

The pattern you will write hundreds of times is this: call the thing, check the error, deal with it, move on.

text, err := readFile("save.txt")
if err != nil {
	fmt.Println("couldn't read it:", err)
	return
}
// from here, text is good

People complain about how much of a Go program is if err != nil. They are not wrong about the volume. But look at what it is doing: every place your program can fail is written out where you can see it, in order, rather than jumping somewhere else the way exceptions do in other languages. It is the same argument lux made for Result, with the checking left to you.

Why copies appear

One oddity in the translation deserves an explanation, because otherwise it looks like the machine being paranoid.

pack := append([]string{}, w.items...)

That says: make a brand new list and pour w.items into it. Why not just use w.items?

Because in lux, handing a value to a function gives that function a copy — always, with no way to share. Go does that for numbers, strings, and structs, but not for lists. A Go list is a handle pointing at storage somewhere else, so two names can end up meaning the same list, and changing one changes the other.

That is precisely the thing lux made impossible, and it is why the translation copies: it is keeping the promise your lux program was written against. You will also see copyWorld(w) for the same reason, since a World holds a list.

Once you are writing your own Go you get to decide. Sharing is often what you want and it is cheaper. Just know that it is now a choice you are making, rather than one the language made for you.

There is a fuller version of this idea, written for people who already program, in No references, anywhere on the design notes.

The walls, gone

The things that told you lux had run out are all ordinary here.

Text that behaves. The reason typing paris got you marked wrong. lux stops at splitting and searching; Go's strings package picks up where it left off:

answer == strings.ToLower("Paris")   // true

Looking a room up instead of matching down a list. That is the map from earlier, and it is one of Go's three built-in shapes.

More than one file. Go programs are made of packages, and splitting your keep across several files is the normal thing rather than a special one.

And the thing Go is actually famous for. Two people exploring the same keep at once, over a network, was the example the graduation guide used for something lux would never do. Go was built for exactly that, and doing two things at once is a single keyword:

go handlePlayer(conn)

That starts a piece of work running alongside everything else. It is the reason a lot of people pick Go, and it is genuinely one of the easier versions of a famously hard idea. It is also the point where you will want to read properly rather than guess.

Where to go from here

Build the thing you were blocked on, in Go, starting from the keep you already have. You will look things up as you need them and remember them because you wanted them first.

Three things to reach for early. Methods, meaning a function attached to your own type — you have already seen the mechanism in those isRoom stubs. Interfaces used properly, which is Go's big idea and worth meeting on purpose rather than only as the thing your enum turned into. And gofmt, the formatter that every Go program goes through, because using it from the first day saves you ever forming an opinion about layout.

One warning worth repeating. Go trusts you more than lux did, and that trust is not a compliment about your carefulness — it is a design position about what a language should police. Two of the guarantees you have been leaning on are gone: nothing counts your cases, and nothing makes you check for missing. Both of those were doing real work. Keep doing that work yourself and Go is a pleasant, fast, sturdy language. Assume it is still being done for you and you will ship the two bugs it stopped catching.

In your terminal: lux convert go on anything you want to see translated. On the web: When lux Feels Small is the guide to the move itself, Swift is the same trip into the language that changes least and Rust into the one that keeps the most, and What lux Leaves Out explains why lux was shaped the way it was.