Appendix B: Coming From Another Language
From Lux
Lux was built to be outgrown, and Go is one of the three languages it was built to be outgrown into. The syntax is close enough that most Lux programs are a mechanical rewrite. The data model is not, and the differences are all in one direction: Lux was strict about a few things Go leaves to you.
| Lux | Go |
|---|---|
let x = 5 | x := 5 — Go has no immutable local |
var x = 5 | x := 5 |
int, float, string, bool | int, float64, string, bool |
[int] | []int |
[[int]] | [][]int |
queue += 4 | queue = append(queue, 4) |
length(xs) | len(xs) |
func f(n: int) -> int { } | func f(n int) int { } |
print("x is", x) | fmt.Println("x is", x) |
eprint(msg) | fmt.Fprintln(os.Stderr, msg) |
for x in xs | for _, x := range xs |
for i in 0..5 | for i := range 5 |
while c | for c |
if a && b | if a && b |
struct Point { x: int } | type Point struct { X int } |
Point(x: 3, y: 4) | Point{X: 3, Y: 4} |
enum Shape { circle(radius: float) } | an interface plus one struct per case |
match s { circle(let r) => ... } | switch v := s.(type) { case Circle: ... } |
Option<int>, some(x), none | (int, bool), or *int and nil |
Result<int, string>, ok(x), err(e) | (int, error), nil, errors.New(e) |
parseInt(s) | strconv.Atoi(s), returning a value and an error |
int(3.9), string(42) | int(3.9), strconv.Itoa(42) |
contains(s, sub) | strings.Contains(s, sub) |
readFile(path) | os.ReadFile(path), returning bytes and an error |
args() | os.Args[1:] |
run(cmd) | exec.Command(...).Output() |
func main() { } | func main() { }, and it must be in package main |
The mechanical differences are the type after the name with no colon, func
without an arrow, capitalized names for anything another package should see, and
braces on the same line as the thing they open. Fix those and a Lux program is
close to Go.
Four differences are real.
There is no let. Every local variable in Go can be reassigned. const
exists but only for compile-time constants, so it cannot hold the result of a
function call. The habit Lux taught you — reach for the name that cannot change
— has no direct spelling here, and what replaces it is scope: declare the
variable as late as possible, in the smallest block that needs it, so there is
less room for anything to reassign it.
There is nil. Lux removed null and Go put it back, and a nil pointer
dereference is Go’s most common runtime failure. Chapter 5.3 covers the habits
that stand in for the guarantee.
Enums become interfaces, and nothing checks your cases. lux convert go on
an enum produces an interface with an unexported marker method and one struct
per case, and turns match into a type switch. That is the right translation
and it is what Chapter 9.3 teaches. What it cannot reproduce is exhaustiveness:
Lux refuses to run a match that misses a case, and Go’s type switch quietly
falls through to default. This is the single largest thing you are giving up,
and it is better learned here than in a debugger.
Option becomes two values or a pointer. Lux’s some/none maps onto
either the comma-ok pattern, v, ok := m[k], or a pointer that may be nil, which
is what lux convert go emits. The comma-ok form is preferred in Go because it
cannot be dereferenced by accident.
You also have a tool. lux convert go program.lux prints the Go translation of
anything you have written, and reading a few of those against the originals is
the fastest way to learn the mapping in terms of code you already understand.
The output is deliberately literal and it declares helper functions a Go
programmer would not, so read it as a translation rather than as a model.
From Python
| Python | Go |
|---|---|
x = 5 | x := 5 |
def f(n): return n * 2 | func f(n int) int { return n * 2 } |
print(f"x is {x}") | fmt.Printf("x is %d\n", x) |
[1, 2, 3] | []int{1, 2, 3} |
{"a": 1} | map[string]int{"a": 1} |
{1, 2} | map[int]struct{}{1: {}, 2: {}} — no set type |
(1, "two") | a struct, or two return values |
for x in xs: | for _, x := range xs { |
for i in range(5): | for i := range 5 { |
[f(x) for x in xs if p(x)] | a loop, or slices plus a helper |
len(xs) | len(xs) |
xs.append(v) | xs = append(xs, v) |
d.get(k) | v, ok := d[k] |
None | nil, and only for the types that have it |
try / except | an error return and if err != nil |
raise | return err, or panic for a bug |
with open(p) as f: | defer f.Close() |
class C: | type C struct plus methods |
isinstance(x, C) | a type switch, usually a sign to use an interface |
int("42") | strconv.Atoi("42"), returning a value and an error |
"".join(xs) | strings.Join(xs, "") |
sorted(xs) | slices.Sorted(...), or slices.Sort in place |
pip install | go get |
The adjustments that take longest: every type is fixed before the program runs,
integers overflow instead of growing, a missing dictionary key gives you the
zero value rather than an exception, indentation means nothing, and there is no
truthiness so every condition must be a real bool.
From JavaScript
| JavaScript | Go |
|---|---|
const x = 5 | x := 5 — Go’s const is compile-time only |
let x = 5 | x := 5 |
function f(n) { return n * 2 } | func f(n int) int { return n * 2 } |
n => n * 2 | func(n int) int { return n * 2 } |
[1, 2, 3] | []int{1, 2, 3} |
{a: 1} as a record | a struct |
{} as a lookup table | a map |
xs.map(f) | a loop, or a generic helper you write once |
xs.length | len(xs) |
xs.push(v) | xs = append(xs, v) |
null and undefined | nil, and only one of it |
try / catch | an error return and if err != nil |
throw | return err, or panic for a bug |
=== | ==, which is the only kind there is |
typeof x | %T, or a type switch |
JSON.parse | json.Unmarshal, in the standard library |
async / await | goroutines and channels, with no function coloring |
Promise.all | a sync.WaitGroup |
npm install | go get |
The adjustments: numbers are not all floats and integer division truncates, ==
refuses to compare different types rather than converting them, there is no
prototype chain, and concurrency is not a keyword on the function — any function
can be called with go and there is no split between async and sync code.