Skip to content

A tour of Lyth

Lyth is a thin layer over Go: the same runtime, standard library, packages, goroutines, and performance, spelled like Go where Go has the concept, with a few additions and a type system that catches nil dereferences and ignored errors at compile time. This tour shows the language by example.

Examples on this site have up to three tabs. Lyth is the source, Go is what lyth gen produces for it with the current compiler, and Output is what the program prints. Switching to the Go tab on any example switches all of them, so you can read a whole page in either language. The rules behind each feature are in the language reference.

func main() {
name := "Lyth"
println("Hello, ${name}!")
}
import "fmt"
func main() {
name := "Lyth"
fmt.Println("Hello, " + name + "!")
}
Hello, Lyth!

Files end in .lyth. println prints to standard output, and ${...} puts any expression into a string. The template becomes plain string concatenation, with strconv for numbers, rather than a fmt.Sprintf call.

func main() {
limit := 3 // declares; the type is inferred
count := 0
while count < limit {
count++
}
ratio: float64 = 2.5
println(count, ratio * float64(count))
}
import "fmt"
func main() {
limit := 3
count := 0
for count < limit {
count++
}
ratio := 2.5
fmt.Println(count, ratio*float64(count))
}
3 7.5

:= declares a variable and = assigns to it, as in Go. Types are inferred, or written after the name: ratio: float64 = 2.5, or total: int for the zero value. The types are Go’s (int, float64, string, bool, …), and numbers never convert implicitly: ratio * count would be an error that suggests float64(count). Local variables can be reassigned; parameters, loop variables, and a method’s receiver are read-only (types). A declaration that hides a variable of an enclosing scope draws a warning. At package level, variables are declared with var and constants with const.

import "strings"
func area(w: float64, h: float64) = w * h
func Greet(name: string, times: int = 1, sep: string = ", "): string {
parts: []string = []
for _ in 0..<times {
parts = append(parts, "hi ${name}")
}
return strings.Join(parts, sep)
}
func main() {
println(area(3, 4))
println(Greet("Ada"), Greet("Ada", 2), Greet("Ada", times = 3, sep = " "))
}
import (
"fmt"
"strings"
)
func area(w float64, h float64) float64 { return w * h }
func Greet(name string, times int, sep string) string {
parts := []string{}
for range times {
parts = append(parts, "hi "+name)
}
return strings.Join(parts, sep)
}
func main() {
fmt.Println(area(3, 4))
fmt.Println(Greet("Ada", 1, ", "), Greet("Ada", 2, ", "), Greet("Ada", 3, " "))
}
12
hi Ada hi Ada, hi Ada hi Ada hi Ada hi Ada

Functions are declared with func; parameters are written name: Type, and the result type follows a colon. A block body returns with return; a body that is one expression can use =, and an unexported one can leave out its result type, which is the expression’s (float64 here). Visibility works as in Go: a name that starts with an uppercase letter (Greet) is exported, visible to other packages, and its result type is always written out because it is part of the package’s API. A lower-case name (area) stays inside its package. The name you write is the name in the generated Go, in stack traces, and in go doc.

Parameters are read-only and may have default values. Calls to Lyth functions can name their arguments, after any positional ones and in any order. The Go function keeps a plain signature: each call passes the defaults it leaves out. Go packages are imported by their import path and used with their exact Go names, and take positional arguments only.

import "fmt"
struct Point {
X: int
Y: int
}
func Point.plus(o: Point): Point = Point(X = X + o.X, Y = Y + o.Y)
func *Point.moveBy(dx: int) {
X += dx
}
// String makes Point a fmt.Stringer, so printing uses it.
func Point.String(): string = "(${X}, ${Y})"
func main() {
p := Point(1, 2)
p.moveBy(10)
println(p.plus(Point(X = 1, Y = 1)))
fmt.Printf("%v and %+v\n", p, p)
}
import (
"fmt"
"strconv"
)
type Point struct {
X int `json:"x"`
Y int `json:"y"`
}
func (p Point) plus(o Point) Point { return Point{X: p.X + o.X, Y: p.Y + o.Y} }
func (p *Point) moveBy(dx int) {
p.X += dx
}
// String makes Point a fmt.Stringer, so printing uses it.
func (p Point) String() string { return "(" + strconv.Itoa(p.X) + ", " + strconv.Itoa(p.Y) + ")" }
func main() {
p := Point{X: 1, Y: 2}
p.moveBy(10)
fmt.Println(p.plus(Point{X: 1, Y: 1}))
fmt.Printf(`%v and %+v
`, p, p)
}
(12, 3)
(11, 2) and (11, 2)

A struct is constructed by calling it, with named arguments in any order or positional arguments for all fields. Methods are declared outside the struct, as in Go: func Point.plus has a value receiver and func *Point.moveBy a pointer receiver. Inside a method, the receiver is self, and fields and methods are also in scope by name (X is self.X). A value receiver is a copy, so assigning to its fields is a compile error; func *T.M() declares a pointer receiver, which sees and can change the caller’s value. There are no static methods: a constructor is a package function such as NewPoint. Capitalized fields are exported, as in Go, and get json tags in lower camel case (X becomes "x", UserID becomes "userID"), so structs serialize the way JSON APIs expect. Lower-case fields stay private to the package and are not serialized.

func main() {
primes := [2, 3, 5, 7]
squares: []int = []
for p in primes {
squares = append(squares, p * p)
}
ages := ["ada": 36, "linus": 54]
ages["guido"] = 67
println(squares, len(ages), 5 in primes, "grace" !in ages)
for i, p in primes {
if i >= 2 {
println("primes[${i}] =", p)
}
}
}
import (
"fmt"
"slices"
"strconv"
)
func main() {
primes := []int{2, 3, 5, 7}
squares := []int{}
for _, p := range primes {
squares = append(squares, p*p)
}
ages := map[string]int{"ada": 36, "linus": 54}
ages["guido"] = 67
v := squares
v2 := len(ages)
v3 := slices.Contains(primes, 5)
_, ok := ages["grace"]
fmt.Println(v, v2, v3, !ok)
for i, p := range primes {
if i >= 2 {
fmt.Println("primes["+strconv.Itoa(i)+"] =", p)
}
}
}
[4 9 25 49] 3 true true
primes[2] = 5
primes[3] = 7

[]T is a Go slice and map[K]V a Go map, with Go’s built-ins (len, append, delete, make) and Go’s packages (slices, maps). A slice shares its backing array, as in Go, so append and element assignments can be visible through other slices of the same array. In a slice type, ? belongs to the element: []User? holds nullable users. for x in xs ranges over elements, for i, x in xs over index and element, for k, v in m over keys and values, and for i in a..<b over numbers. An empty literal needs a type: [] or [:] with an annotation. Where nothing gives the type, write it as Go does: []int64{1, 2}, map[string]any{"status": "ok", "count": 3}.

x in c tests membership and !in negates it: an element of a list (slices.Contains), a key of a map (_, ok := m[k]), a substring (strings.Contains), or a number in a range (code in 200..299, two comparisons).

func fib(n: int): int {
a, b := 0, 1
for _ in 0..<n {
a, b = b, a + b
}
return a
}
func main() {
println(fib(10))
grid := [[1, 2], [3, 4], [5, 6]]
outer@ for i, row in grid {
for v in row {
if v == 4 {
println("found 4 in row", i)
break@outer
}
}
}
}
import "fmt"
func fib(n int) int {
a, b := 0, 1
for range n {
a, b = b, a+b
}
return a
}
func main() {
fmt.Println(fib(10))
grid := [][]int{{1, 2}, {3, 4}, {5, 6}}
outer:
for i, row := range grid {
for _, v := range row {
if v == 4 {
fmt.Println("found 4 in row", i)
break outer
}
}
}
}
55
found 4 in row 1

for iterates over a collection, a channel, or a range, and while c { } loops on a condition (while true forever). As in Go, conditions take no parentheses and bodies are always blocks. a, b := 0, 1 declares several variables, and an assignment list evaluates all values on the right before assigning any, so a, b = b, a + b needs no temporary. _ discards a loop variable. outer@ labels a loop, and break@outer or continue@outer jumps out of the loops inside it; both become Go labels.

import "slices"
import "strings"
func mapAll(xs: []int, f: (int) -> int): []int {
out: []int = []
for x in xs {
out = append(out, f(x))
}
return out
}
func main() {
println(mapAll([1, 2, 3], { x -> x * 10 }))
println(mapAll([1, 2, 3]) { it + 1 })
words := ["kiwi", "fig", "banana"]
slices.SortFunc(words, { a, b -> len(a) - len(b) })
println(strings.Join(words, ","))
}
import (
"fmt"
"slices"
"strings"
)
func mapAll(xs []int, f func(int) int) []int {
out := []int{}
for _, x := range xs {
out = append(out, f(x))
}
return out
}
func main() {
fmt.Println(mapAll([]int{1, 2, 3}, func(x int) int {
return x * 10
}))
fmt.Println(mapAll([]int{1, 2, 3}, func(it int) int {
return it + 1
}))
words := []string{"kiwi", "fig", "banana"}
slices.SortFunc(words, func(a string, b string) int {
return len(a) - len(b)
})
fmt.Println(strings.Join(words, ","))
}
[10 20 30]
[2 3 4]
fig,kiwi,banana

A lambda is { params -> body }. Parameter types come from where the lambda is used, including generic Go functions such as slices.SortFunc. A lambda with one parameter can leave it unnamed and use it, and a lambda that is the last argument can follow the parentheses. Lambdas are Go closures.

func size(n: int): string = if n < 10 { "small" } else { "large" }
func describe(code: int): string = when code {
200, 204 -> "ok"
404 -> "not found"
in 500..599 -> "server error"
else -> "status ${code}"
}
func sign(n: int): string = when {
n < 0 -> "negative"
n == 0 -> "zero"
else -> "positive"
}
func main() {
println(size(3), size(30))
println(describe(204), describe(404), describe(503), describe(302))
println(sign(-2), sign(0), sign(7))
}
import (
"fmt"
"strconv"
)
func size(n int) string {
if n < 10 {
return "small"
}
return "large"
}
func describe(code int) string {
if code == 200 || code == 204 {
return "ok"
}
if code == 404 {
return "not found"
}
if code >= 500 && code <= 599 {
return "server error"
}
return "status " + strconv.Itoa(code)
}
func sign(n int) string {
switch {
case n < 0:
return "negative"
case n == 0:
return "zero"
default:
return "positive"
}
}
func main() {
fmt.Println(size(3), size(30))
fmt.Println(describe(204), describe(404), describe(503), describe(302))
fmt.Println(sign(-2), sign(0), sign(7))
}
small large
ok not found server error status 302
negative zero positive

As in Go, conditions and subjects take no parentheses and bodies are always blocks, so an if used as a value is if c { a } else { b }. A condition cannot end in a trailing lambda; parenthesize it: if (xs.Any { it > 0 }) { ... }. when with a subject compares it with each condition, or tests it with is and in; without one, each condition is a boolean. Branches never fall through. Used as a value, both forms need an else. When the value is returned, the generated Go returns from each branch.

struct User {
Name: string
Email: string?
}
func find(id: int): User? {
when id {
1 -> return User(Name = "Ada", Email = "ada@example.com")
2 -> return User(Name = "Linus", Email = null)
}
return null
}
func main() {
for id in 1..3 {
user := find(id)
if user == null {
println(id, "nobody")
continue
}
// here user is a User, not a User?
println(id, user.Name, user.Email ?: "no email")
}
println(find(2)?.Name, find(3)?.Name)
}
import "fmt"
type User struct {
Name string `json:"name"`
Email *string `json:"email"`
}
func find(id int) *User {
switch id {
case 1:
return &User{Name: "Ada", Email: new("ada@example.com")}
case 2:
return &User{Name: "Linus", Email: nil}
}
return nil
}
func main() {
for id := 1; id <= 3; id++ {
user := find(id)
if user == nil {
fmt.Println(id, "nobody")
continue
}
v3 := user.Name
var v string
v2 := user.Email
if v2 != nil {
v = *v2
} else {
v = "no email"
}
fmt.Println(id, v3, v)
}
v4 := find(2)
var name *string
if v4 != nil {
name = new(v4.Name)
}
v5 := find(3)
var name2 *string
if v5 != nil {
name2 = new(v5.Name)
}
s := "null"
if name != nil {
s = *name
}
s2 := "null"
if name2 != nil {
s2 = *name2
}
fmt.Println(s, s2)
}
1 Ada ada@example.com
2 Linus no email
3 nobody
Linus null

A type with ? can be null; others cannot, so user.Name on a User? is a compile error until a check proves user is not null (a smart cast). Smart casts apply to parameters and to local variables the function never reassigns; for a field or a reassigned variable, copy the value into a new local first. ?. reads through a possible null, ?: supplies a default or exits early (?: return), and !! asserts. In Go, User? is *User and string? is *string, which encoding/json reads as null and omits with omitempty. Values from Go APIs have unknown nullability; see Go interop.

import "errors"
import "strconv"
var ErrNegative = errors.New("negative quantity")
func parseQuantity(s: string): int throws {
n := strconv.Atoi(s)?
if n < 0 {
throw ErrNegative
}
return n
}
func total(items: []string): int throws {
sum := 0
for item in items {
sum += parseQuantity(item)?
}
return sum
}
func main() {
println(total(["1", "2", "3"]) catch { -1 })
n := total(["4", "x"]) catch (err) {
println("error:", err)
0
}
println(n)
total(["-5"]) catch (err) {
println("negative?", errors.Is(err, ErrNegative))
}
}
import (
"errors"
"fmt"
"strconv"
)
var ErrNegative = errors.New("negative quantity")
func parseQuantity(s string) (int, error) {
n, err := strconv.Atoi(s)
if err != nil {
return 0, err
}
if n < 0 {
return 0, ErrNegative
}
return n, nil
}
func total(items []string) (int, error) {
sum := 0
for _, item := range items {
v, err := parseQuantity(item)
if err != nil {
return 0, err
}
sum += v
}
return sum, nil
}
func main() {
v, err := total([]string{"1", "2", "3"})
if err != nil {
v = -1
}
fmt.Println(v)
n, err := total([]string{"4", "x"})
if err != nil {
fmt.Println("error:", err)
n = 0
}
fmt.Println(n)
if _, err := total([]string{"-5"}); err != nil {
fmt.Println("negative?", errors.Is(err, ErrNegative))
}
}
6
error: strconv.Atoi: parsing "x": invalid syntax
0
negative? true

A function that can fail is declared throws; in Go it returns an extra error. Postfix ? returns the error to the caller, throw returns a new one, and catch handles it in place, either with a fallback value or with any code. Go functions returning (T, error) work the same way, so strconv.Atoi(s)? is ordinary Lyth. Ignoring an error silently is a compile error. Errors are Go error values, so errors.Is, errors.As, and wrapping work unchanged:

import "os"
func readConfig(path: string): string throws {
data := os.ReadFile(path) catch (err) { throw err.wrap("reading config ${path}") }
return string(data)
}
func main() throws {
println(readConfig("missing.toml")?)
}
import (
"fmt"
"os"
)
func readConfig(path string) (string, error) {
data, err := os.ReadFile(path)
if err != nil {
return "", fmt.Errorf("reading config %s: %w", path, err)
}
return string(data), nil
}
func run() error {
v, err := readConfig("missing.toml")
if err != nil {
return err
}
fmt.Println(v)
return nil
}
func main() {
if err := run(); err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
}
error: reading config missing.toml: open missing.toml: no such file or directory
(exit status 1)

err.wrap("...") becomes fmt.Errorf("...: %w", err). A main that throws prints the error and exits with status 1. To handle one type of error and pass on the rest, catch (e: *NotFound) { ... }? matches like errors.AsType (an example). The details are in errors.

interface Shape {
Area(): float64
}
struct Circle : Shape {
R: float64
}
func Circle.Area(): float64 = 3 * R * R
struct Rect {
W: float64
H: float64
}
func Rect.Area(): float64 = W * H
func describe(s: Shape): string = when s {
is Circle -> "circle of radius ${s.R}"
is Rect -> "${s.W}x${s.H} rectangle"
else -> "some shape"
}
func main() {
shapes: []Shape = [Circle(R = 1), Rect(W = 2, H = 3)]
for s in shapes {
println(describe(s), s.Area())
}
first := shapes[0] as? Rect
println(first == null)
}
import (
"fmt"
"strconv"
)
type Shape interface {
Area() float64
}
type Circle struct {
R float64 `json:"r"`
}
var _ Shape = Circle{}
func (c Circle) Area() float64 { return 3 * c.R * c.R }
type Rect struct {
W float64 `json:"w"`
H float64 `json:"h"`
}
func (r Rect) Area() float64 { return r.W * r.H }
func describe(s Shape) string {
switch s := s.(type) {
case Circle:
return "circle of radius " + strconv.FormatFloat(s.R, 'g', -1, 64)
case Rect:
return strconv.FormatFloat(s.W, 'g', -1, 64) + "x" + strconv.FormatFloat(s.H, 'g', -1, 64) + " rectangle"
default:
return "some shape"
}
}
func main() {
shapes := []Shape{Circle{R: 1}, Rect{W: 2, H: 3}}
for _, s := range shapes {
fmt.Println(describe(s), s.Area())
}
var first *Rect
if rect, ok := shapes[0].(Rect); ok {
first = new(rect)
}
fmt.Println(first == nil)
}
circle of radius 1 3
2x3 rectangle 6
true

Interfaces are structural, exactly as in Go: a type implements one by having its methods, with the same names (Area here, capitalized because the interface’s method is exported). struct Circle : Shape declares the intent. The compiler then checks the methods at the struct, and the Go code gets the usual var _ Shape = Circle{} assertion; without the declaration, a missing method shows up where a Circle is first used as a Shape. is tests a dynamic type and smart-casts, so s.R works inside the branch; a when of is tests becomes a Go type switch. as casts (panicking on failure) and as? yields null instead.

enum Suit { Hearts, Diamonds, Clubs, Spades }
func Suit.color(): string = when self {
Hearts, Diamonds -> "red"
Clubs, Spades -> "black"
}
sealed interface Payment {
Cents(): int
}
struct Card : Payment {
Last4: string
Amount: int
}
func Card.Cents(): int = Amount
struct Voucher : Payment {
Code: string
}
func Voucher.Cents(): int = 500
func receipt(p: Payment): string = when p {
is Card -> "card ending ${p.Last4}"
is Voucher -> "voucher ${p.Code}"
}
func main() {
println(Suit.Spades, Suit.Spades.color())
payments: []Payment = [Card(Last4 = "4242", Amount = 1999), Voucher(Code = "WELCOME")]
for p in payments {
println(receipt(p), p.Cents())
}
}
import (
"fmt"
"strconv"
)
type Suit int
const (
SuitHearts Suit = iota
SuitDiamonds
SuitClubs
SuitSpades
)
// String returns the name of the Suit entry.
func (s Suit) String() string {
switch s {
case SuitHearts:
return "Hearts"
case SuitDiamonds:
return "Diamonds"
case SuitClubs:
return "Clubs"
case SuitSpades:
return "Spades"
}
return "Suit(" + strconv.Itoa(int(s)) + ")"
}
func (s Suit) color() string {
switch s {
case SuitHearts, SuitDiamonds:
return "red"
case SuitClubs, SuitSpades:
return "black"
default:
panic("lyth: no when branch for this Suit (example.lyth:3:29)")
}
}
type Payment interface {
Cents() int
isPayment()
}
type Card struct {
Last4 string `json:"last4"`
Amount int `json:"amount"`
}
var _ Payment = Card{}
func (Card) isPayment() {}
func (c Card) Cents() int { return c.Amount }
type Voucher struct {
Code string `json:"code"`
}
var _ Payment = Voucher{}
func (Voucher) isPayment() {}
func (v Voucher) Cents() int { return 500 }
func receipt(p Payment) string {
switch p := p.(type) {
case Card:
return "card ending " + p.Last4
case Voucher:
return "voucher " + p.Code
default:
panic("lyth: no when branch for this Payment (example.lyth:25:36)")
}
}
func main() {
fmt.Println(SuitSpades, SuitSpades.color())
payments := []Payment{Card{Last4: "4242", Amount: 1999}, Voucher{Code: "WELCOME"}}
for _, p := range payments {
fmt.Println(receipt(p), p.Cents())
}
}
Spades black
card ending 4242 1999
voucher WELCOME 500

An enum is a closed set of values. It compiles to Go’s iota constants (SuitHearts, SuitSpades, …) and a String method, so it prints by name. A sealed interface is a closed set of types: only structs of the same package that declare it (struct Card : Payment) implement it.

The point of both is the exhaustive when. A when over an enum or a sealed interface must handle every entry or member type, or have an else; it then needs no else to be used as a value. Add an entry to Suit or a member to Payment, and every when that does not handle it stops compiling, which is the check a Go switch cannot give. Inside a when over an enum, entries may be written without the type (Hearts).

import "cmp"
import "lyth/coll"
func maxOf[T: cmp.Ordered](a: T, b: T): T = if a > b { a } else { b }
struct Page[T] {
Items: []T
Total: int
}
func Page[T].Empty(): bool = len(Items) == 0
func string.Shout(): string = self + "!"
struct Todo {
Title: string
Done: bool
}
func main() {
println(maxOf(3, 7), maxOf("go", "lyth"))
todos := [Todo(Title = "spec", Done = true), Todo(Title = "ship", Done = false)]
open := Page(Items = todos.Filter { !it.Done }.Map { it.Title }, Total = len(todos))
println(open.Items, open.Empty(), "done".Shout())
}
import (
"cmp"
"fmt"
"lyth/coll"
)
func maxOf[T cmp.Ordered](a T, b T) T {
if a > b {
return a
}
return b
}
type Page[T any] struct {
Items []T `json:"items"`
Total int `json:"total"`
}
func (p Page[T]) Empty() bool { return len(p.Items) == 0 }
func Shout(s string) string { return s + "!" }
type Todo struct {
Title string `json:"title"`
Done bool `json:"done"`
}
func main() {
fmt.Println(maxOf(3, 7), maxOf("go", "lyth"))
todos := []Todo{{Title: "spec", Done: true}, {Title: "ship", Done: false}}
open := Page[string]{Items: coll.Map(coll.Filter(todos, func(it Todo) bool {
return !it.Done
}), func(it Todo) string {
return it.Title
}), Total: len(todos)}
fmt.Println(open.Items, open.Empty(), Shout("done"))
}
7 lyth
[ship] false done!

Type parameters go in square brackets after the name, as in Go, with a colon before the constraint: func maxOf[T: cmp.Ordered] is Go’s func maxOf[T cmp.Ordered], and [T] alone means [T: any]. A constraint is an interface such as comparable, cmp.Ordered, or fmt.Stringer. A generic type’s methods name its parameters in the receiver (func Page[T].Empty()). Type arguments are inferred, here from the arguments and from the fields given to Page(...), or written out: Page[string](https://github.com/lyth-lang/lyth/tree/main/docs/guide/...), maxOf[int](3, 7).

func string.Shout() is an extension function: called like a method, it is a plain Go function taking the receiver first, Shout("done"), and its body reaches the receiver through self. The standard library’s lyth/coll provides collection operations this way (Filter, Map, GroupBy, SortedBy, Take, …). Each returns a new slice and never changes its receiver, so a loop is still the cheapest choice on a hot path; the Collection API lists them with their costs.

import "sync"
func main() {
wg := sync.WaitGroup()
mu := sync.Mutex()
total := 0
for i in 1..10 {
wg.Add(1)
go {
defer wg.Done()
mu.Lock()
total += i
mu.Unlock()
}
}
wg.Wait()
println(total)
}
import (
"fmt"
"sync"
)
func main() {
wg := sync.WaitGroup{}
mu := sync.Mutex{}
total := 0
for i := 1; i <= 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
mu.Lock()
total += i
mu.Unlock()
}()
}
wg.Wait()
fmt.Println(total)
}
55

go and defer take a call or a block. Concurrency is Go’s: goroutines, sync, context, and errgroup all work directly.

import "time"
struct Job {
ID: int
}
func produce(jobs: chan<- *Job, n: int) {
defer jobs.close()
for i in 1..n {
jobs.send(&Job(ID = i))
}
}
func main() {
jobs := make(chan *Job, 2)
go { produce(jobs, 3) }
while true {
job := jobs.receive() ?: break
println("job", job.ID)
}
results := make(chan string)
go {
time.Sleep(time.Second)
results.send("slow answer")
}
select {
r := results.receive() -> println(r)
time.After(10 * time.Millisecond).receive() -> println("gave up waiting")
}
}
import (
"fmt"
"time"
)
type Job struct {
ID int `json:"id"`
}
func produce(jobs chan<- *Job, n int) {
defer close(jobs)
for i := 1; i <= n; i++ {
jobs <- &Job{ID: i}
}
}
func main() {
jobs := make(chan *Job, 2)
go func() {
produce(jobs, 3)
}()
for {
var job *Job
v := <-jobs
if v == nil {
break
}
job = v
fmt.Println("job", job.ID)
}
results := make(chan string)
go func() {
time.Sleep(time.Second)
results <- "slow answer"
}()
select {
case r := <-results:
fmt.Println(r)
case <-time.After(10 * time.Millisecond):
fmt.Println("gave up waiting")
}
}
job 1
job 2
job 3
gave up waiting

Channels are Go channels with method syntax: make(chan T, n) creates one, ch.send(v), ch.receive(), and ch.close() lower to ch <- v, <-ch, and close(ch), and for v in ch receives until the channel is closed. chan<- T and <-chan T restrict a parameter to one direction. Semantics are Go’s: receiving from a closed channel does not fail, it yields the zero value. That is why receive() on a channel of pointers is nullable, and why ?: break ends the loop once produce closes jobs. v, ok := ch.receive() tells a zero value from a closed channel.

select waits for whichever channel operation can proceed first. Its branches look like when branches: a receive can bind its value, else runs when no operation is ready, and break exits the enclosing loop.