PONY λ M2 Modula-2

Dart.CodeCompared.To/Go

An interactive executable cheatsheet comparing Dart and Go

Dart 3.7 Go 1.26.2
Basics & Running
Hello, World
Go's skeleton is always package main, an import block, and func main() — and each Go cell here shows that complete program, assembled around the lines you would actually write.
void main() { print('Hello, World!'); }
fmt.Println("Hello, World!")
The full Go equivalent is package main plus import "fmt" plus func main() { fmt.Println("Hello, World!") }. Printing lives in the fmt package rather than being built in, names are exported by capitalization (Println is public because it is capitalized), and Go's formatter uses tabs — the tooling, not the programmer, decides layout.
Declarations & :=
void main() { final answer = 42; var counter = 0; counter += 1; print(answer + counter); }
answer := 42 // := infers the type and declares var counter = 0 // 'var' also works; explicit type optional counter += 1 fmt.Println(answer + counter)
Go's := is the everyday short declaration — it declares and infers in one step, like Dart's var with inference. There is no final/const distinction for locals (Go's const is for compile-time constants only, covered later). Two Go rules bite immediately: an unused local variable is a compile error, and so is an unused import.
No ternary operator
void main() { final score = 72; final grade = score >= 60 ? 'pass' : 'fail'; // ternary print(grade); }
score := 72 grade := "fail" if score >= 60 { // no ?: — use an if statement grade = "pass" } fmt.Println(grade)
Go deliberately omits the ternary ?:, so Dart's cond ? a : b becomes a small if statement. It is more verbose, and intentionally so — the language favors one obvious way to write each thing. Note Go requires braces on every if and forbids parentheses around the condition.
Numbers: explicit conversion
void main() { int whole = 7 ~/ 2; // integer division operator double exact = 7 / 2; // / always yields a double double mixed = 3 + 0.5; // int promotes to double automatically print('$whole $exact $mixed'); }
whole := 7 / 2 // int / int truncates -> 3 exact := 7.0 / 2 // a float64 -> 3.5 count := 3 mixed := float64(count) + 0.5 // NO implicit int->float; convert explicitly fmt.Println(whole, exact, mixed)
Like Swift, Go does no implicit numeric conversion: float64(count) + 0.5 must convert, where Dart quietly promotes an int. There is no ~// on two integers already truncates — and no num supertype. Go's numeric types are explicit about size (int, int64, float64, byte), and mixing them without a conversion will not compile.
Errors as Values
No exceptions — errors are values
The single biggest change from Dart. Go has no throw/try/catch for ordinary failures: a function that can fail returns an error as its last value, and you check it right there.
int parsePort(String text) { final value = int.tryParse(text); if (value == null) throw FormatException('bad port: $text'); return value; } void main() { try { print(parsePort('nope')); } catch (error) { print('caught: $error'); } }
func parsePort(text string) (int, error) { value, err := strconv.Atoi(text) if err != nil { return 0, fmt.Errorf("bad port: %s", text) } return value, nil } port, err := parsePort("nope") if err != nil { // the ubiquitous Go idiom fmt.Println("caught:", err) } else { fmt.Println(port) }
A fallible Go function returns (value, error), and the caller inspects err immediately — the if err != nil you will write hundreds of times. Where Dart lets an exception propagate up the stack automatically, Go makes every failure explicit and local. The upside is that the control flow is visible; the cost is verbosity that the language considers a fair trade.
Custom errors & wrapping
class NotFound implements Exception { final String key; NotFound(this.key); @override String toString() => 'not found: $key'; } void main() { try { throw NotFound('user-7'); } on NotFound catch (error) { print(error); } }
type NotFound struct{ Key string } func (e NotFound) Error() string { // satisfies the error interface return "not found: " + e.Key } func lookup() error { return fmt.Errorf("lookup failed: %w", NotFound{Key: "user-7"}) } err := lookup() var notFound NotFound if errors.As(err, &notFound) { // unwrap to the concrete type fmt.Println("missing key:", notFound.Key) }
Any type with an Error() string method is an error — the same implicit-interface rule as everything else in Go. You wrap context around an error with fmt.Errorf("…: %w", err), then test the chain with errors.Is (matches a sentinel) or errors.As (extracts a concrete type). This replaces Dart's on FormatException catch type-based dispatch with value inspection.
try/finally → defer
void main() { print('open'); try { print('work'); } finally { print('close'); // always runs } }
func process() { fmt.Println("open") defer fmt.Println("close") // runs when process() returns fmt.Println("work") } process()
Dart's finally becomes Go's defer: a deferred call runs when the surrounding function returns, no matter how. It is the idiomatic way to pair cleanup with acquisition — file, _ := os.Open(name); defer file.Close() — placing the cleanup right next to the setup. Multiple defers run in last-in, first-out order.
throw → panic / recover
void main() { try { throw StateError('unrecoverable'); } catch (error) { print('recovered: $error'); } }
func risky() (result string) { defer func() { if r := recover(); r != nil { // catch a panic result = fmt.Sprintf("recovered: %v", r) } }() panic("unrecoverable") } fmt.Println(risky())
Go does have panic/recover, but they are not Dart's throw/catch — they are reserved for truly unrecoverable situations (programmer bugs, impossible states), not ordinary error flow. recover only works inside a defer. If you find yourself reaching for panic to signal a normal failure, Go's answer is: return an error instead.
Structs, not Classes
Classes → structs + methods
class Rectangle { final int width; final int height; Rectangle(this.width, this.height); int area() => width * height; } void main() { print(Rectangle(3, 4).area()); }
type Rectangle struct { Width int Height int } // A method is a function with a RECEIVER, declared outside the struct. func (r Rectangle) Area() int { return r.Width * r.Height } fmt.Println(Rectangle{Width: 3, Height: 4}.Area())
Go has no class. Data lives in a struct, and methods are declared separately as functions with a receiverfunc (r Rectangle) Area(). There is no constructor keyword and no this; the receiver name (r) is whatever you choose. Fields and methods are exported by capitalization, so Width and Area are public.
Values, pointers & receivers
Every Dart object is a reference. Go has value types (a struct copies on assignment) and explicit pointers (*T, &x) — and a method must take a pointer receiver to mutate.
class Counter { int value = 0; void increment() => value++; } void main() { final counter = Counter(); // a reference counter.increment(); print(counter.value); // 1 }
type Counter struct{ Value int } // Pointer receiver (*Counter) so the change is visible to the caller. func (c *Counter) Increment() { c.Value++ } counter := Counter{} counter.Increment() // Go auto-takes &counter for the pointer receiver fmt.Println(counter.Value) // 1
A value receiver (func (c Counter)) operates on a copy and cannot mutate the original; a pointer receiver (func (c *Counter)) can. Go conveniently auto-references counter.Increment() as (&counter).Increment() for you. This value/pointer split — invisible in Dart, where everything is already a reference — is the thing to stay conscious of when passing structs around.
Inheritance → embedding
class Animal { String describe() => 'an animal'; } class Dog extends Animal { String bark() => 'Woof'; } void main() { final dog = Dog(); print(dog.describe()); // inherited print(dog.bark()); }
type Animal struct{} func (a Animal) Describe() string { return "an animal" } type Dog struct { Animal // embedded — Dog gets Describe() by composition } func (d Dog) Bark() string { return "Woof" } dog := Dog{} fmt.Println(dog.Describe()) // promoted from the embedded Animal fmt.Println(dog.Bark())
Go has no inheritance. Instead you embed one struct in another, and the embedded type's methods are promoted so dog.Describe() works — composition that reads like inheritance but is not. There is no super, no overriding in the polymorphic sense, and no class hierarchy; behavior is shared by embedding structs and satisfying interfaces.
Implicit Interfaces
Interfaces are satisfied implicitly
abstract class Speaker { String speak(); } class Dog implements Speaker { // must declare 'implements' @override String speak() => 'Woof'; } void main() { Speaker who = Dog(); print(who.speak()); }
type Speaker interface { Speak() string } type Dog struct{} // No 'implements' clause — having the method IS the conformance. func (d Dog) Speak() string { return "Woof" } var who Speaker = Dog{} // Dog satisfies Speaker structurally fmt.Println(who.Speak())
Go interfaces are satisfied implicitly: any type with the right methods conforms, with no implements declaration anywhere — structural, like TypeScript, but for method sets. This means you can define an interface after the types that satisfy it, and make types you do not own conform. Interfaces are kept small (often one method), and accepting an interface rather than a concrete type is the idiomatic way to decouple.
dynamic → any & type switch
void main() { final List<Object> mixed = [1, 'two', 3.0]; for (final item in mixed) { final label = switch (item) { int _ => 'int', String _ => 'string', _ => 'other', }; print(label); } }
mixed := []any{1, "two", 3.0} // any == interface{} for _, item := range mixed { switch item.(type) { // type switch case int: fmt.Println("int") case string: fmt.Println("string") default: fmt.Println("other") } }
Go's any (an alias for the empty interface interface{}) is the closest thing to Dart's Object/dynamic — it holds any value. To get back to a concrete type you use a type assertion (item.(int), with a comma-ok form) or a type switch as above, which parallels Dart's type-pattern switch. Reaching for any is discouraged, though; Go prefers a specific interface or a generic (later).
nil & Zero Values
No null safety — nil & panics
Dart's sound null safety is a compile-time guarantee. Go has none: pointers, slices, maps, interfaces, and functions can be nil, and the compiler will not stop you from dereferencing one.
class Node { int value; Node? next; Node(this.value, [this.next]); } void main() { final head = Node(1); // print(head.next.value); // won't compile: next may be null print(head.next?.value ?? -1); // safe: null-aware }
type Node struct { Value int Next *Node } head := Node{Value: 1} // fmt.Println(head.Next.Value) // COMPILES, then PANICS at run time (nil pointer) if head.Next != nil { // you must guard by hand fmt.Println(head.Next.Value) } else { fmt.Println(-1) }
There is no ? type, no ?., and no ?? — and crucially no compile-time protection. A nil pointer dereference compiles cleanly and panics at run time, which is exactly the class of bug Dart's null safety was built to eliminate. The discipline Go asks for is manual if x != nil guards; the zero-value design below softens how often you need them.
Zero values, not uninitialized
void main() { int count; // unassigned // print(count); // won't compile: must be assigned first count = 0; String name = ''; print('$count "$name"'); }
var count int // zero value: 0 var name string // zero value: "" var ready bool // zero value: false var next *int // zero value: nil (a pointer) fmt.Printf("%d %q %v %v\n", count, name, ready, next)
Every Go variable is usable immediately because it starts at its type's zero value0, "", false, nil, or a struct with all-zero fields. There is no "used before assigned" error like Dart's, because there is no uninitialized state. Well-chosen zero values (a ready-to-use sync.Mutex, an empty-but-usable bytes.Buffer) are a deliberate design goal in Go APIs.
Functions & Multiple Returns
Records → multiple returns
(int, int) divmod(int a, int b) => (a ~/ b, a % b); void main() { final (quotient, remainder) = divmod(17, 5); print('$quotient $remainder'); }
func divmod(a, b int) (int, int) { // two return values return a / b, a % b } quotient, remainder := divmod(17, 5) fmt.Println(quotient, remainder) // _ discards a value you do not want (and the compiler requires it). q, _ := divmod(17, 5) fmt.Println(q)
Multiple return values are a built-in language feature in Go, not a tuple or record type — func divmod(a, b int) (int, int). It reads like Dart's record return and destructure, but the values are separate, and you must consume each one: assign an unwanted result to the blank identifier _. This is the mechanism behind the ubiquitous (value, error) pattern.
No named/optional/default args
String connect(String host, {int port = 5432, bool tls = true}) => '$host:$port tls=$tls'; void main() { print(connect('db.example')); print(connect('db.example', port: 6432, tls: false)); }
type Options struct { Port int TLS bool } func connect(host string, opts Options) string { return fmt.Sprintf("%s:%d tls=%v", host, opts.Port, opts.TLS) } fmt.Println(connect("db.example", Options{Port: 5432, TLS: true})) fmt.Println(connect("db.example", Options{Port: 6432, TLS: false}))
Go has no named arguments, no default parameter values, and no optional parameters — a stark contrast with Dart's rich parameter syntax. The idiomatic replacement is an options struct (as above), whose fields double as named, defaultable arguments via their zero values. For libraries, the "functional options" pattern (variadic ...Option functions) is the other common answer.
Variadic functions & closures
int sumAll(List<int> numbers) => numbers.fold(0, (total, each) => total + each); void main() { print(sumAll([1, 2, 3, 4])); }
func sumAll(numbers ...int) int { // variadic: any number of ints total := 0 for _, each := range numbers { total += each } return total } fmt.Println(sumAll(1, 2, 3, 4)) // call with separate args, no slice literal
Go's variadic parameter numbers ...int collects trailing arguments into a slice, so callers write sumAll(1, 2, 3, 4) with no list literal — something Dart cannot express, since Dart has no varargs. Closures work as in Dart (functions are values that capture their environment), and you can spread an existing slice into a variadic call with sumAll(nums...).
Slices, Maps & Arrays
List → slice (append)
void main() { final numbers = <int>[1, 2, 3]; numbers.add(4); print(numbers); print(numbers.length); }
numbers := []int{1, 2, 3} numbers = append(numbers, 4) // append RETURNS the (possibly new) slice fmt.Println(numbers) fmt.Println(len(numbers))
A Dart List is a Go slice ([]int), the everyday growable sequence. The surprise is append: it may allocate a new backing array and returns the result, so you must write numbers = append(numbers, 4) — ignoring the return value is a classic Go bug. Length is the free function len(numbers), not a property.
Slices are views
void main() { final original = [10, 20, 30, 40]; final middle = original.sublist(1, 3); // a COPY in Dart middle[0] = 99; print(original); // [10, 20, 30, 40] — unchanged }
original := []int{10, 20, 30, 40} middle := original[1:3] // a VIEW, sharing the backing array middle[0] = 99 fmt.Println(original) // [10 99 30 40] — the write showed through
A Go slice expression original[1:3] creates a view over the same backing array, not a copy, so writing through the sub-slice mutates the original — the opposite of Dart's sublist, which copies. This makes slicing cheap but aliasing-prone; when you need an independent copy, allocate one with make and copy(dst, src).
Map → map & comma-ok
void main() { final ages = {'Ada': 36, 'Grace': 45}; print(ages['Ada']); // 36 print(ages['Nobody']); // null print(ages.containsKey('Grace')); }
ages := map[string]int{"Ada": 36, "Grace": 45} fmt.Println(ages["Ada"]) // 36 fmt.Println(ages["Nobody"]) // 0 — the ZERO value, not nil! age, ok := ages["Grace"] // comma-ok: was the key present? fmt.Println(age, ok)
A Go map[string]int is Dart's Map, but a missing key returns the value type's zero value (0 here), not null — so you cannot tell "absent" from "present but zero" by the value alone. The comma-ok form, age, ok := ages["Grace"], gives you the presence flag Dart's containsKey provides. A nil map reads fine but panics on write, so create maps with make or a literal.
map/where → explicit loops
void main() { final numbers = [1, 2, 3, 4, 5, 6]; final evensDoubled = numbers.where((n) => n.isEven).map((n) => n * 2).toList(); print(evensDoubled); // [4, 8, 12] }
numbers := []int{1, 2, 3, 4, 5, 6} evensDoubled := []int{} for _, n := range numbers { if n%2 == 0 { // filter, then transform — by hand evensDoubled = append(evensDoubled, n*2) } } fmt.Println(evensDoubled) // [4 8 12]
Go long resisted functional collection helpers, so the idiomatic transform is an explicit for/range loop rather than Dart's fluent where/map chain. Go 1.21+ added a slices package (and 1.23 iterators) with some helpers, but the loop remains the norm — the language prizes a visible, allocation-obvious transformation over a chained one.
Control Flow
for is the only loop
void main() { for (var i = 0; i < 3; i++) { print(i); } var n = 3; while (n > 0) { print(n); n--; } }
for i := 0; i < 3; i++ { // classic three-part for fmt.Println(i) } n := 3 for n > 0 { // 'for' with only a condition IS the while loop fmt.Println(n) n-- }
Go has exactly one loop keyword: for. The three-part form is familiar, a condition-only for n > 0 is the while Go does not have, and a bare for { } loops forever. To iterate values you use for index, value := range collection, which covers slices, maps, strings, and channels — the workhorse you will reach for constantly.
switch: no fallthrough
String size(int n) { switch (n) { case 0: return 'none'; case 1: case 2: return 'few'; default: return 'many'; } } void main() => print(size(2));
func size(n int) string { switch n { case 0: return "none" case 1, 2: // comma-separated, no fallthrough return "few" default: return "many" } } fmt.Println(size(2))
Go's switch does not fall through by default — each case breaks on its own, so no break is needed (the explicit fallthrough keyword exists but is rare). Multiple values share a case with commas. A bare switch { case cond: … } with no subject acts as a clean if/else if ladder, and cases can be any type, not just constants.
if with an init statement
int? tryParse(String text) => int.tryParse(text); void main() { final value = tryParse('42'); if (value != null) { print('got $value'); } }
if value, err := strconv.Atoi("42"); err == nil { fmt.Println("got", value) // value is scoped to the if/else }
Go's if can carry an init statement before the condition — if value, err := …; err == nil — and the variables it declares are scoped to the if/else only. This keeps a short-lived result (very often the (value, err) pair) from leaking into the surrounding scope, and it is one of Go's most-used idioms for tidy error handling.
Constants & iota
enum → const + iota
enum Direction { north, east, south, west } void main() { final heading = Direction.east; print(heading.name); // 'east' print(Direction.values.length); // 4 }
type Direction int const ( North Direction = iota // 0 East // 1 (iota increments per line) South // 2 West // 3 ) func (d Direction) String() string { return []string{"north", "east", "south", "west"}[d] } heading := East fmt.Println(heading) // "east" via the String() method
Go has no enum type. The idiom is a named integer type plus a const block using iota, which auto-increments down the block (0, 1, 2, …). A String() method gives readable printing, standing in for Dart's .name. It is more manual than a Dart enum — there is no free values list and no exhaustiveness check — but it compiles to plain integers.
Goroutines & Channels
Future/isolate → goroutine
Go's concurrency is its headline feature and its biggest departure from Dart. A goroutine is a function launched with go — a cheap thread that shares memory with the rest of the program, so no function is async and blocking is free.
Future<int> load(int id) async => id * 10; void main() async { final results = await Future.wait([load(1), load(2), load(3)]); print(results); // [10, 20, 30] }
results := make([]int, 3) var wg sync.WaitGroup for i := 0; i < 3; i++ { wg.Add(1) go func(id int) { // launch a goroutine defer wg.Done() results[id] = (id + 1) * 10 }(i) } wg.Wait() // block until all goroutines finish fmt.Println(results) // [10 20 30]
Where Dart marks async work with Future/async/await, Go writes ordinary synchronous functions and runs them concurrently with go. There is no coloring of functions and no await; you coordinate completion with a sync.WaitGroup (a counter) or channels. Each goroutine here writes its own slot, so no lock is needed — but because goroutines share memory, that safety is your responsibility, unlike Dart isolates.
Channels: no Dart analog
// Dart coordinates async work through Futures and Streams; there is no // built-in synchronous hand-off primitive quite like a Go channel. Stream<int> produce() async* { for (var i = 1; i <= 3; i++) yield i; } void main() async { await for (final n in produce()) print(n); }
numbers := make(chan int) // a channel of ints go func() { for i := 1; i <= 3; i++ { numbers <- i // send blocks until a receiver is ready } close(numbers) // signal "no more values" }() for n := range numbers { // receive until the channel is closed fmt.Println(n) }
A channel is a typed pipe that goroutines use to communicate and synchronize — send with ch <- v, receive with <-ch, and an unbuffered channel makes the sender wait for a receiver (a rendezvous). It is Go's motto in code: "share memory by communicating." Ranging over a channel drains it until it is closed, which loosely parallels consuming a Dart Stream — but the hand-off is a first-class language primitive, not a library type.
Isolates → shared memory + Mutex
Dart isolates share nothing and copy messages, so there are no data races. Go goroutines share one heap, so guarding mutable state is on you — with a channel, or a sync.Mutex.
import 'dart:isolate'; Future<int> heavySum(int n) { return Isolate.run(() { // separate heap, nothing shared var total = 0; for (var i = 1; i <= n; i++) total += i; return total; }); } void main() async { print(await heavySum(1000)); // 500500 }
var total int var mu sync.Mutex var wg sync.WaitGroup for i := 1; i <= 1000; i++ { wg.Add(1) go func(n int) { defer wg.Done() mu.Lock() // guard the shared counter total += n mu.Unlock() }(i) } wg.Wait() fmt.Println(total) // 500500
Because every goroutine touches the same total, the unguarded total += n would be a genuine data race — so the sync.Mutex is doing load-bearing work (Go's race detector, go run -race, will flag it if you forget). A Dart programmer coming from isolates must relearn that shared mutable state is possible and must be protected. Go's preferred alternative is often to pass ownership over a channel instead of locking.
Generics
Generics & constraints
T firstOr<T>(List<T> items, T fallback) => items.isEmpty ? fallback : items.first; void main() { print(firstOr<int>([], -1)); print(firstOr(['a', 'b'], 'z')); }
func FirstOr[T any](items []T, fallback T) T { if len(items) == 0 { return fallback } return items[0] } fmt.Println(FirstOr([]int{}, -1)) fmt.Println(FirstOr([]string{"a", "b"}, "z"))
Go gained generics in 1.18, and the shape is familiar: type parameters in square brackets, func FirstOr[T any](…), with any as the unconstrained bound. Constraints are expressed as interfaces — the built-in comparable, or constraints.Ordered for <, or any interface you define. Inference usually lets you omit the explicit [int]. Generics arrived late and are used sparingly; a concrete type or a plain interface is still often preferred.
Strings, Bytes & Runes
Interpolation → Printf/Sprintf
void main() { final name = 'Ada'; final year = 1843; print('$name wrote code in $year.'); final line = '${name.toUpperCase()} ($year)'; print(line); }
name := "Ada" year := 1843 fmt.Printf("%s wrote code in %d.\n", name, year) // verbs, not interpolation line := fmt.Sprintf("%s (%d)", strings.ToUpper(name), year) fmt.Println(line)
Go has no string interpolation. You format with fmt.Printf (prints) or fmt.Sprintf (returns a string) using verbs — %s for strings, %d for integers, %v for anything, %q for a quoted string. It is closer to C's printf than to Dart's $name, and the compiler does not check that your verbs match the arguments (though go vet does).
Strings are UTF-8 bytes
void main() { final text = 'café'; print(text.length); // 4 (UTF-16 code units) print(text.runes.length); // 4 code points }
text := "café" fmt.Println(len(text)) // 5 — len counts BYTES (é is 2) fmt.Println(utf8.RuneCountInString(text)) // 4 — actual characters for i, r := range text { // range yields runes, not bytes fmt.Printf("%d:%c ", i, r) } fmt.Println()
A Go string is an immutable sequence of bytes holding UTF-8, so len(text) is a byte count — 5 for "café", because é takes two bytes — not a character count. To count characters you use utf8.RuneCountInString, and ranging over a string yields runes (Unicode code points) with their byte offsets. Dart's String is UTF-16 with its own surrogate-pair caveats, but the byte-vs-character gap here surprises most newcomers.
Packages & Modules
Libraries → packages & exports
// geometry.dart // double areaOfCircle(double r) => 3.14159 * r * r; // public // double _helper(double r) => r * r; // private (_) // // main.dart import 'geometry.dart'; void main() { print(areaOfCircle(2)); }
// A Go package is a DIRECTORY of files sharing one namespace. Exporting is // by capitalization, not a keyword or an underscore: // package geometry // func AreaOfCircle(r float64) float64 { return 3.14159 * r * r } // exported // func helper(r float64) float64 { return r * r } // unexported // // Then: import "example.com/geometry" and call geometry.AreaOfCircle(2) func AreaOfCircle(r float64) float64 { return 3.14159 * r * r } fmt.Println(AreaOfCircle(2))
Go organizes code into packages (a directory of files that share a namespace), imported by path, where Dart imports individual files. Visibility is decided by capitalization: AreaOfCircle is exported because it starts with a capital letter, helper is package-private because it does not — replacing Dart's leading-underscore convention with a rule you can see at every call site. The Dart side references a second file, so its column is display-only.
pub → go modules
// pubspec.yaml // dependencies: // http: ^1.2.0 // // Then: dart pub get // Then: import 'package:http/http.dart' as http; void main() { print('Dependencies are declared in pubspec.yaml and fetched by pub.'); }
// go.mod // module example.com/myapp // go 1.26 // require github.com/go-chi/chi/v5 v5.1.0 // // Then: go get, or 'go mod tidy' to sync imports <-> go.mod fmt.Println("Dependencies live in go.mod and are fetched by the go tool.")
Dart's pubspec.yaml plus dart pub get corresponds to a go.mod file plus the go tool (go get, go mod tidy). Import paths are URLs (github.com/…) resolved directly from source hosts rather than a central registry like pub.dev, and go mod tidy keeps go.mod in sync with the imports your code actually uses.