PONY λ M2 Modula-2

Go.CodeCompared.To/Dart

An interactive executable cheatsheet comparing Go and Dart

Go 1.26.2 Dart 3.7
Output & Running
Hello, World
package main import "fmt" func main() { fmt.Println("Hello, World!") }
void main() { print('Hello, World!'); }
Dart requires an explicit void main() function as the program entry point, just like Go's func main(). The print() function is the standard way to write a line to stdout — the equivalent of fmt.Println. Dart uses single or double quotes for strings; the style convention is single quotes.
Running a program
// Run source directly: // go run main.go // // Compile to a binary: // go build -o hello main.go // ./hello
// Run source directly (JIT mode): // dart run main.dart // // Compile to a native binary (AOT): // dart compile exe main.dart -o hello // ./hello // // Compile to JavaScript: // dart compile js main.dart -o hello.js
Dart supports both JIT (just-in-time, for development) and AOT (ahead-of-time, for production) compilation, similar to how Go's go run is for development and go build produces a binary. Dart AOT-compiled binaries are self-contained native executables like Go binaries. Dart also compiles to JavaScript (for web targets) and to Arm/x86 machine code — a flexibility Go does not offer.
Formatted output
package main import "fmt" func main() { name := "Dart" version := 3.7 fmt.Printf("Language: %s %.1f\n", name, version) fmt.Printf("Name: %-10s | Version: %04.1f\n", name, version) }
void main() { final name = 'Dart'; final version = 3.7; print('Language: $name ${version.toStringAsFixed(1)}'); // Dart has no printf-style verbs — use string methods: print('Name: ${name.padRight(10)} | Version: ${version.toStringAsFixed(1).padLeft(4, '0')}'); }
Dart has no Printf or format verbs. String interpolation ($variable or ${expression}) combined with methods like toStringAsFixed(), padLeft(), and padRight() replaces most fmt.Printf use cases. For complex padding, padLeft(width, character) fills with a given character.
Comments
package main import "fmt" func main() { // Single-line comment /* Block comment */ count := 0 fmt.Println(count) }
void main() { // Single-line comment /* Block comment */ /// Doc comment — used by dart doc to generate API documentation. /// Supports Markdown formatting. int count = 0; print(count); }
Dart uses the same // and /* ... */ comment syntax as Go. It adds a third form, /// Doc comment, which is the standard for documenting public APIs and is processed by dart doc into HTML documentation (analogous to godoc comments in Go, which use ordinary // lines immediately before declarations).
Variables & Types
Declaring variables
package main import "fmt" func main() { var explicit int = 42 inferred := "hello" const maxConnections = 100 fmt.Println(explicit, inferred, maxConnections) }
void main() { int explicit = 42; // explicit type var inferred = 'hello'; // type inferred as String const maxConnections = 100; // compile-time constant final runtimeValue = DateTime.now().millisecondsSinceEpoch; // runtime constant print(explicit); print(inferred); print(maxConnections); print(runtimeValue); }
Dart splits what Go calls const into two keywords: const for values known at compile time (like Go's const) and final for values set once at runtime and never reassigned (Go has no direct equivalent). Go's short declaration := maps to Dart's var with a type-inferred initializer. Dart types appear before the name, in the C-style tradition Go also follows.
Basic types
package main import "fmt" func main() { var count int = 10 var price float64 = 9.99 var active bool = true var name string = "Alice" fmt.Println(count, price, active, name) }
void main() { int count = 10; double price = 9.99; bool active = true; String name = 'Alice'; print(count); print(price); print(active); print(name); }
Dart's primitive types map cleanly to Go's: int (arbitrary precision on native, 64-bit on the VM), double (64-bit IEEE 754, replaces Go's float64), bool, and String. Note that Dart has a single int type (no int32, int64, uint8, etc.) and a single double — there is no float32.
Type conversion
package main import ( "fmt" "strconv" ) func main() { number := 42 asFloat := float64(number) asString := strconv.Itoa(number) back, _ := strconv.Atoi("123") fmt.Println(asFloat, asString, back) }
void main() { int number = 42; double asDouble = number.toDouble(); String asString = number.toString(); int parsed = int.parse('123'); double parsedDouble = double.parse('3.14'); print(asDouble); print(asString); print(parsed); print(parsedDouble); }
Dart uses method-style conversions (number.toDouble(), number.toString()) rather than Go's C-style cast syntax (float64(number)). Parsing strings uses static methods: int.parse() and double.parse() — which throw FormatException on failure rather than returning a second value like Go's strconv.Atoi(). Use int.tryParse() to get null instead of an exception.
<code>dynamic</code> and <code>Object?</code>
package main import "fmt" func main() { var anything interface{} = 42 anything = "now a string" anything = []int{1, 2, 3} fmt.Println(anything) }
void main() { dynamic anything = 42; anything = 'now a string'; anything = [1, 2, 3]; print(anything); // Object? is the safer alternative — accepts any non-null value + null Object? safeAny = 'hello'; if (safeAny is String) { print(safeAny.length); // automatically promoted to String inside the if } }
Dart's dynamic is the rough equivalent of Go's interface{} (now any): a variable that can hold any type. However, Dart also offers Object? as a safer alternative — like dynamic but with compile-time checks that prevent calling unknown methods. The is operator narrows the type within a conditional block, similar to a Go type assertion.
Multiple return values → Records
package main import "fmt" func minMax(numbers []int) (int, int) { minimum, maximum := numbers[0], numbers[0] for _, number := range numbers[1:] { if number < minimum { minimum = number } if number > maximum { maximum = number } } return minimum, maximum } func main() { minimum, maximum := minMax([]int{3, 1, 4, 1, 5, 9, 2, 6}) fmt.Println(minimum, maximum) }
(int, int) minMax(List<int> numbers) { int minimum = numbers[0]; int maximum = numbers[0]; for (final number in numbers.sublist(1)) { if (number < minimum) minimum = number; if (number > maximum) maximum = number; } return (minimum, maximum); } void main() { final (minimum, maximum) = minMax([3, 1, 4, 1, 5, 9, 2, 6]); print('$minimum $maximum'); }
Dart 3 introduced Records ((int, int)) as the equivalent of Go's multiple return values. Records are anonymous value types: return (minimum, maximum) creates one, and destructuring with final (minimum, maximum) = ... unpacks it. Records can also have named fields: ({int min, int max}). Before Dart 3, you had to return a List or a custom class.
Null Safety
nil vs null
package main import "fmt" func findUser(identifier int) *string { if identifier == 1 { name := "Alice" return &name } return nil // nil: valid for pointers, interfaces, slices, maps } func main() { result := findUser(1) if result != nil { fmt.Println(*result) } missing := findUser(99) fmt.Println(missing) // prints: <nil> }
// In Dart, non-nullable types cannot hold null at all. // A compile error, not a runtime panic, catches the mistake. String findUser(int identifier) { if (identifier == 1) return 'Alice'; return 'unknown'; // must return a String — cannot return null } String? findUserNullable(int identifier) { if (identifier == 1) return 'Alice'; return null; // String? means "String or null" } void main() { print(findUser(1)); final result = findUserNullable(1); if (result != null) { print(result); // result promoted to String inside this block } final missing = findUserNullable(99); print(missing); // prints: null }
In Go, nil is a valid value for pointers, interfaces, slices, maps, channels, and functions — and a nil dereference causes a runtime panic. In Dart, null safety is enforced at compile time: a String can never hold null, only String? can. The compiler tracks nullability through every expression and prevents unguarded use of a nullable value.
Nullable types (<code>?</code>)
package main import "fmt" type Config struct { Host string Port *int // nil means "use default" Timeout *int } func main() { port := 8080 config := Config{Host: "localhost", Port: &port} if config.Timeout == nil { fmt.Println("timeout: default") } }
class Config { final String host; final int? port; // int? means "int or null" final int? timeout; // null means "use default" const Config({required this.host, this.port, this.timeout}); } void main() { const config = Config(host: 'localhost', port: 8080); if (config.timeout == null) { print('timeout: default'); } // The non-null port is available directly — no * dereference needed: print(config.port); // 8080 }
Where Go uses pointer types (*int) to express "optional" fields, Dart uses int? — the ? suffix makes any type nullable. There is no pointer dereference: if you check config.port != null, Dart automatically promotes the type to non-nullable int inside the if block. Struct fields that may be absent are much cleaner to express in Dart.
Null-aware operators
package main import "fmt" func getLabel(pointer *string) string { if pointer == nil { return "unknown" } return *pointer } func main() { var namePointer *string fmt.Println(getLabel(namePointer)) name := "Alice" fmt.Println(len(name)) }
void main() { String? possiblyNull; // ?? — use the right side if left is null final label = possiblyNull ?? 'unknown'; print(label); // unknown // ??= — assign only if currently null possiblyNull ??= 'default'; print(possiblyNull); // default // ?. — call method only if not null; returns null if null String? name = 'Alice'; print(name?.length); // 5 name = null; print(name?.length); // null (no exception) // ! — assert non-null (throws if null — use sparingly) String nonNull = 'hello'; print(nonNull!.length); // 5 }
Dart's null-aware operators replace the boilerplate if x == nil guards that appear throughout Go code: ?? provides a fallback value (like Go's common if x == nil { x = default } pattern), ?. short-circuits to null instead of panicking (Go has no equivalent), and ??= is a conditional assignment. The ! operator asserts non-null and panics if wrong — similar to a forced nil dereference in Go.
<code>late</code> variables
package main import "fmt" type Service struct { connection *string // initialized lazily, nil until first use } func (service *Service) connect() { conn := "db://localhost:5432" service.connection = &conn } func (service *Service) query() string { if service.connection == nil { service.connect() } return "result from " + *service.connection } func main() { service := &Service{} fmt.Println(service.query()) }
class Service { late String connection; // not nullable, but initialized lazily void connect() { connection = 'db://localhost:5432'; } String query() { // late fields are initialized on first access — no null check needed if (!_connected) { connect(); _connected = true; } return 'result from $connection'; } bool _connected = false; } void main() { final service = Service(); print(service.query()); }
Dart's late keyword declares a non-nullable variable that will be set before it is first read, with the check deferred to runtime. This avoids the Go pattern of using a pointer solely to express "not yet initialized" — the field is still String, not String?. If a late variable is read before assignment, Dart throws a LateInitializationError.
Strings
String literals
package main import "fmt" func main() { single := "double quotes only in Go" escaped := "line one\nline two\ttabbed" fmt.Println(single) fmt.Println(escaped) }
void main() { // Dart allows single or double quotes (single preferred by convention) final greeting = 'Hello, World!'; final also = "also valid"; // Escape sequences work the same way (\n = newline, \t = tab) final escaped = 'line one\nline two\ttabbed'; // Raw strings — no escape processing (like Go's backtick strings) final path = r'C:\Users\alice\Documents'; print(greeting); print(also); print(escaped); print(path); }
Go allows only double-quoted strings; Dart allows both single and double quotes interchangeably (the Dart style guide prefers single quotes). Dart's raw strings are prefixed with rr'C:\Users\alice' — which behaves exactly like Go's backtick raw strings: backslashes are literal and no escape sequences are processed.
String interpolation
package main import "fmt" func main() { name := "Alice" age := 30 message := fmt.Sprintf("Hello, %s! You are %d years old.", name, age) fmt.Println(message) }
void main() { final name = 'Alice'; final age = 30; final message = 'Hello, $name! You are $age years old.'; print(message); // Use ${...} for expressions: final items = [1, 2, 3]; print('Count: ${items.length}, doubled: ${age * 2}'); }
Dart has native string interpolation: $variable for simple names, ${expression} for any Dart expression. This replaces Go's fmt.Sprintf for the common case of building strings. No format verbs are used — if you need precise numeric formatting, call methods like toStringAsFixed(2) inside ${}.
Multiline strings
package main import "fmt" func main() { // Go uses \n escape sequences — no multiline string literal syntax template := "Dear user,\n\nYour account is ready.\n\nRegards" fmt.Println(template) }
void main() { // Triple-quoted strings span multiple lines with full interpolation final name = 'Alice'; final template = """ Dear $name, Your account is ready. Regards"""; print(template); // Single-quoted triple strings also work: final message = ''' line one line two '''; print(message); }
Dart supports triple-quoted strings ("""...""" or '''...''') for multiline content — strings that span multiple lines without any special escaping. Unlike Go's backtick strings, triple-quoted Dart strings still support interpolation and escape sequences. The opening triple-quote is typically followed by a newline, and the content starts on the next line.
Common string operations
package main import ( "fmt" "strings" ) func main() { message := "Hello, World!" fmt.Println(strings.ToUpper(message)) fmt.Println(strings.Contains(message, "World")) fmt.Println(strings.Replace(message, "World", "Dart", 1)) fmt.Println(strings.Split(message, ", ")) fmt.Println(len(message)) }
void main() { final message = 'Hello, World!'; // Methods live on the String object — no separate package needed print(message.toUpperCase()); print(message.contains('World')); print(message.replaceFirst('World', 'Dart')); print(message.split(', ')); print(message.length); // Additional conveniences: print(message.startsWith('Hello')); print(message.trim()); print(' spaces '.trimLeft()); }
In Go, most string operations live in the strings package as free functions: strings.ToUpper(s), strings.Contains(s, substr). In Dart, they are methods on the String type itself: message.toUpperCase(), message.contains('World'). This means a Dart import is rarely needed for basic string work — the core library is always available.
Lists & Collections
Lists (slices)
package main import "fmt" func main() { numbers := []int{3, 1, 4, 1, 5, 9} fmt.Println(numbers) fmt.Println(numbers[0]) fmt.Println(len(numbers)) slice := numbers[1:4] fmt.Println(slice) }
void main() { final numbers = [3, 1, 4, 1, 5, 9]; // List<int> inferred print(numbers); print(numbers[0]); print(numbers.length); final slice = numbers.sublist(1, 4); // sublist, not slicing syntax print(slice); // Typed list literal: final words = <String>['go', 'dart', 'flutter']; print(words); }
Dart's List<T> is the equivalent of Go's slice. Both use zero-based indexing and a length property. The key difference is that Dart has no slice syntax — instead of numbers[1:4], you call numbers.sublist(1, 4). Dart lists are also growable by default (no separate append step needed), while Go's slices require make or append to extend.
List operations
package main import ( "fmt" "sort" ) func main() { items := []string{"banana", "apple", "cherry"} items = append(items, "date") fmt.Println(len(items)) sort.Strings(items) fmt.Println(items) items = append(items[:2], items[3:]...) // remove index 2 fmt.Println(items) }
void main() { final fruits = ['banana', 'apple', 'cherry']; fruits.add('date'); // append one element fruits.addAll(['elderberry', 'fig']); // append multiple print(fruits.length); fruits.sort(); // in-place sort print(fruits); fruits.removeAt(2); // remove by index fruits.remove('fig'); // remove by value print(fruits); print(fruits.contains('apple')); print(fruits.indexOf('apple')); }
Dart's List has a rich method API that replaces many patterns that require manual index arithmetic in Go. Appending uses add() and addAll() instead of append. Removing an element by index is a single removeAt(2) call — there is no equivalent of Go's slice-splice idiom (items[:i] + items[i+1:]...). Sorting calls the in-place sort() method instead of importing a package.
Sets
package main import "fmt" func main() { // Go has no built-in Set — use map[T]struct{} idiom seen := map[string]struct{}{} words := []string{"apple", "banana", "apple", "cherry"} for _, word := range words { seen[word] = struct{}{} } fmt.Println(len(seen)) // 3 unique _, exists := seen["apple"] fmt.Println(exists) }
void main() { // Dart has a built-in Set<T> final colors = {'red', 'green', 'blue', 'red'}; // literal, duplicates ignored print(colors.length); // 3 unique colors.add('yellow'); colors.remove('green'); print(colors.contains('red')); // true print(colors); // Set operations: final primary = {'red', 'blue', 'yellow'}; final warm = {'red', 'orange', 'yellow'}; print(primary.intersection(warm)); // {red, yellow} print(primary.union(warm)); }
Go has no built-in set type — Go idiom is to use a map[T]struct{} and check key presence. Dart has a first-class Set<T> with literals ({'a', 'b'}), automatic deduplication, and built-in operations like intersection(), union(), and difference(). Note: an empty {} is inferred as Map, not Set; use <String>{} for an empty set.
List higher-order methods
package main import "fmt" func main() { numbers := []int{1, 2, 3, 4, 5} // Filter (manual) var evens []int for _, number := range numbers { if number%2 == 0 { evens = append(evens, number) } } fmt.Println(evens) // Map (manual) doubled := make([]int, len(numbers)) for index, number := range numbers { doubled[index] = number * 2 } fmt.Println(doubled) }
void main() { final numbers = [1, 2, 3, 4, 5]; // where() — filter final evens = numbers.where((number) => number.isEven).toList(); print(evens); // map() — transform (returns lazy Iterable; call toList() to materialize) final doubled = numbers.map((number) => number * 2).toList(); print(doubled); // reduce() / fold() final total = numbers.fold(0, (sum, number) => sum + number); print(total); // any() and every() print(numbers.any((number) => number > 4)); print(numbers.every((number) => number > 0)); }
Go has no built-in higher-order list functions — filtering and mapping require manual for loops and append. Dart's Iterable API provides where(), map(), fold(), any(), every(), and more as first-class operations with concise anonymous function syntax. Note that map() returns a lazy Iterable, not a List; call .toList() to materialize it.
Maps
Map literals
package main import "fmt" func main() { scores := map[string]int{ "Alice": 95, "Bob": 87, "Carol": 92, } fmt.Println(scores) fmt.Println(scores["Alice"]) value, exists := scores["Dave"] fmt.Println(value, exists) }
void main() { final scores = { 'Alice': 95, 'Bob': 87, 'Carol': 92, }; // Map<String, int> inferred print(scores); print(scores['Alice']); // 95 // Missing key returns null, not zero + false like Go final missing = scores['Dave']; // null print(missing); // Use containsKey to check existence print(scores.containsKey('Dave')); // false }
Dart map literals use the same curly-brace syntax as Go map literals, but without the map[K]V type prefix. The key difference in lookup: Go returns a zero value and a bool (value, ok := m[key]), while Dart returns the value or null for a missing key. Since keys can legitimately map to null, use containsKey() to distinguish "key absent" from "key maps to null".
Map operations
package main import "fmt" func main() { inventory := map[string]int{"apples": 5} inventory["oranges"] = 3 // add inventory["apples"] = 10 // update delete(inventory, "apples") fmt.Println(len(inventory)) fmt.Println(inventory) }
void main() { final inventory = <String, int>{'apples': 5}; inventory['oranges'] = 3; // add inventory['apples'] = 10; // update inventory.remove('apples'); // delete — returns the removed value print(inventory.length); print(inventory); // putIfAbsent — add only if key is missing (replaces the Go "check then set" pattern) inventory.putIfAbsent('bananas', () => 7); print(inventory); // update — transform an existing value inventory.update('oranges', (count) => count + 1, ifAbsent: () => 1); print(inventory); }
Dart's Map uses method calls where Go uses built-in syntax: inventory.remove(key) instead of Go's delete(map, key); length is inventory.length rather than len(inventory). The putIfAbsent() and update() methods condense the common Go pattern of reading a value, checking existence, then writing — into a single call.
Map iteration
package main import ( "fmt" "sort" ) func main() { capitals := map[string]string{ "France": "Paris", "Japan": "Tokyo", "Brazil": "Brasilia", } keys := make([]string, 0, len(capitals)) for key := range capitals { keys = append(keys, key) } sort.Strings(keys) for _, key := range keys { fmt.Printf("%s: %s\n", key, capitals[key]) } }
void main() { final capitals = { 'France': 'Paris', 'Japan': 'Tokyo', 'Brazil': 'Brasilia', }; // Iterate over entries directly for (final entry in capitals.entries) { print('${entry.key}: ${entry.value}'); } // Keys and values separately: print(capitals.keys.toList()); print(capitals.values.toList()); // Map has forEach, map(), and where() on entries: capitals.forEach((country, capital) => print('$country → $capital')); }
Go iterates maps with for key, value := range m; the iteration order is intentionally random. Dart iterates with for (final entry in map.entries) and the order reflects insertion order (Dart's Map is insertion-ordered since Dart 2). Dart provides entries, keys, and values as views on the map, plus forEach() as a functional alternative.
Control Flow
if/else
package main import "fmt" func classify(score int) string { if score >= 90 { return "A" } else if score >= 80 { return "B" } else if score >= 70 { return "C" } else { return "F" } } func main() { fmt.Println(classify(95)) fmt.Println(classify(82)) }
String classify(int score) { if (score >= 90) { return 'A'; } else if (score >= 80) { return 'B'; } else if (score >= 70) { return 'C'; } else { return 'F'; } } void main() { print(classify(95)); print(classify(82)); }
Dart's if/else syntax is identical to Go's, including the requirement for braces. One difference: Go allows a short initialization statement in the condition (if err := doSomething(); err != nil), while Dart does not support that form. Dart does support if expressions (introduced in Dart 3.3): final label = if (score >= 90) 'A' else 'B'.
for loops
package main import "fmt" func main() { // C-style for for index := 0; index < 5; index++ { fmt.Print(index, " ") } fmt.Println() // while equivalent count := 10 for count > 0 { fmt.Print(count, " ") count -= 3 } fmt.Println() }
void main() { // C-style for — collect results to print on one line final forResults = <int>[]; for (int index = 0; index < 5; index++) { forResults.add(index); } print(forResults.join(' ')); // while final whileResults = <int>[]; int count = 10; while (count > 0) { whileResults.add(count); count -= 3; } print(whileResults.join(' ')); // do-while (Go has no do-while) int attempts = 0; do { attempts++; } while (attempts < 3); print(attempts); }
Dart's C-style for loop and while loop work identically to Go's equivalents (Go's for with one condition is its while loop). Dart adds a do-while loop, which Go famously omits. The example collects values into a list and prints with join() to keep the output on one line without needing dart:io's stdout.write().
for-in / range
package main import "fmt" func main() { fruits := []string{"apple", "banana", "cherry"} for _, fruit := range fruits { fmt.Println(fruit) } for index, fruit := range fruits { fmt.Printf("%d: %s\n", index, fruit) } }
void main() { final fruits = ['apple', 'banana', 'cherry']; // for-in — value only (no index by default) for (final fruit in fruits) { print(fruit); } // Use indexed loop or asMap() for index access for (int index = 0; index < fruits.length; index++) { print('$index: ${fruits[index]}'); } // Or use the entries / asMap() pattern: fruits.asMap().forEach((index, fruit) => print('$index: $fruit')); }
Go's for index, value := range slice always provides both index and value, with _ to discard either. Dart's for (final item in collection) provides only the value — there is no built-in two-argument form. To get the index, use a C-style loop, collection.asMap().forEach(), or the indexed getter (for (final (index, item) in fruits.indexed) in Dart 3.3+).
switch expressions
package main import "fmt" func dayType(day string) string { switch day { case "Saturday", "Sunday": return "weekend" case "Monday", "Tuesday", "Wednesday", "Thursday", "Friday": return "weekday" default: return "unknown" } } func main() { fmt.Println(dayType("Monday")) fmt.Println(dayType("Saturday")) }
String dayType(String day) { return switch (day) { 'Saturday' || 'Sunday' => 'weekend', 'Monday' || 'Tuesday' || 'Wednesday' || 'Thursday' || 'Friday' => 'weekday', _ => 'unknown', }; } void main() { print(dayType('Monday')); print(dayType('Saturday')); // switch can also be a statement (with { case ...: ... }) int code = 200; switch (code) { case 200: print('OK'); case 404: print('Not Found'); default: print('Other'); } }
Dart 3 introduced switch expressions that return a value directly, similar to Rust's match arms. The || operator combines multiple patterns in one arm (replacing Go's comma-separated case list). The wildcard _ serves as the default. Unlike Go, Dart switch cases do not fall through by default — there is no need for break, and there is no fallthrough keyword.
Functions
Basic functions
package main import "fmt" func add(first int, second int) int { return first + second } func greet(name string) { fmt.Printf("Hello, %s!\n", name) } func main() { result := add(3, 4) fmt.Println(result) greet("Dart") }
int add(int first, int second) { return first + second; } void greet(String name) { print('Hello, $name!'); } void main() { final result = add(3, 4); print(result); greet('Dart'); }
Dart function syntax places the return type before the function name (like C and Go) and uses void for functions that return nothing. Unlike Go, Dart puts the type before each parameter name rather than after it — int add(int first, int second) vs Go's func add(first int, second int) int. Adjacent parameters of the same type cannot be grouped: you must write int first, int second, not first, second int.
Named parameters
package main import "fmt" // Go has no named parameters — callers must use positional args func createUser(username string, age int, active bool) { fmt.Printf("user=%s age=%d active=%v\n", username, age, active) } func main() { createUser("alice", 30, true) // Can't call as: createUser(age: 30, username: "alice") }
// Dart named parameters are wrapped in { } void createUser({ required String username, required int age, bool active = true, // with a default value }) { print('user=$username age=$age active=$active'); } void main() { createUser(username: 'alice', age: 30); createUser(age: 25, username: 'bob', active: false); // any order }
Dart supports named parameters by wrapping them in curly braces in the function signature. Named parameters are optional by default; add required to make them mandatory. Callers pass them by name in any order. This is one of Dart's biggest ergonomic improvements over Go, where large function signatures with similar-typed parameters are fragile (callers can silently swap arguments of the same type).
Optional positional parameters
package main import "fmt" // Go: simulate with a sentinel value or variadic func connect(host string, port int) { if port == 0 { port = 5432 } fmt.Printf("Connecting to %s:%d\n", host, port) } func main() { connect("localhost", 0) // caller must pass 0 to use default connect("db.example.com", 5433) }
// Positional optional parameters use [ ] — caller may omit them void connect(String host, [int port = 5432]) { print('Connecting to $host:$port'); } void main() { connect('localhost'); // uses default port 5432 connect('db.example.com', 5433); // explicit port }
Dart offers two kinds of optional parameters: named (with { }) and positional (with [ ]). Positional optional parameters must appear after all required parameters, and callers omit them from the right. Go has no built-in default parameter values; the closest idiom is a sentinel value (pass 0 for int, "" for string) or an options struct.
Arrow / expression body functions
package main import "fmt" func double(number int) int { return number * 2 } func isEven(number int) bool { return number%2 == 0 } func main() { fmt.Println(double(7)) fmt.Println(isEven(4)) }
// Arrow syntax: the => expression is the return value int double(int number) => number * 2; bool isEven(int number) => number % 2 == 0; String describe(int number) => number.isEven ? 'even' : 'odd'; void main() { print(double(7)); print(isEven(4)); print(describe(5)); }
Dart's arrow syntax (=> expression) defines a function that evaluates the expression and returns it — exactly one expression, no return keyword. It is the equivalent of Ruby's endless method (def double(n) = n * 2), and serves the same brevity purpose as Go's single-line function bodies. The arrow is commonly used with callbacks and collection methods.
Closures
Functions as values
package main import "fmt" func apply(numbers []int, transform func(int) int) []int { result := make([]int, len(numbers)) for index, number := range numbers { result[index] = transform(number) } return result } func main() { double := func(number int) int { return number * 2 } numbers := []int{1, 2, 3, 4} fmt.Println(apply(numbers, double)) }
List<int> applyTransform(List<int> numbers, int Function(int) transform) { return numbers.map(transform).toList(); } void main() { // Function stored in a variable int Function(int) double = (number) => number * 2; final numbers = [1, 2, 3, 4]; print(applyTransform(numbers, double)); // Pass inline anonymous function: print(applyTransform(numbers, (number) => number + 10)); }
Both Go and Dart treat functions as first-class values. Dart's function type syntax is int Function(int) (return type first, then the Function keyword with parameter types in parens), compared to Go's func(int) int. In practice, both communities use type inference to avoid writing the type in most local variables, letting the compiler infer it from the initializer.
Closures (capturing variables)
package main import "fmt" func makeCounter() func() int { count := 0 return func() int { count++ return count } } func main() { counter := makeCounter() fmt.Println(counter()) // 1 fmt.Println(counter()) // 2 fmt.Println(counter()) // 3 }
int Function() makeCounter() { int count = 0; return () { count++; return count; }; } void main() { final counter = makeCounter(); print(counter()); // 1 print(counter()); // 2 print(counter()); // 3 }
Closures work identically in Go and Dart: an inner function captures variables from its enclosing scope by reference, and the captured state persists across calls. The syntax differs (func() int { ... } vs () { ... }) but the semantics are the same. Dart closures that have no parameters can use the short form () { ... }; with arrow syntax a zero-parameter closure is () => expression.
Higher-order functions
package main import "fmt" func filter(numbers []int, predicate func(int) bool) []int { var result []int for _, number := range numbers { if predicate(number) { result = append(result, number) } } return result } func main() { numbers := []int{1, 2, 3, 4, 5, 6} evens := filter(numbers, func(number int) bool { return number%2 == 0 }) fmt.Println(evens) }
void main() { final numbers = [1, 2, 3, 4, 5, 6]; // Built-in higher-order methods on List/Iterable final evens = numbers.where((number) => number.isEven).toList(); final doubled = numbers.map((number) => number * 2).toList(); final total = numbers.reduce((sum, number) => sum + number); print(evens); print(doubled); print(total); // Chaining: final sumOfDoubledEvens = numbers .where((number) => number.isEven) .map((number) => number * 2) .reduce((sum, number) => sum + number); print(sumOfDoubledEvens); }
Go's standard library has no generic Filter or Map for slices — you write the loop yourself. (Go 1.23+ adds slices and iter packages with some helpers, but they are not yet universally used.) Dart's Iterable API provides where(), map(), reduce(), and fold() as built-in operations, making functional pipelines idiomatic.
Classes & OOP
Defining a class
package main import "fmt" // Go has no classes — use a struct with methods type Animal struct { name string species string } func (animal Animal) Describe() string { return fmt.Sprintf("%s is a %s", animal.name, animal.species) } func main() { cat := Animal{name: "Whiskers", species: "cat"} fmt.Println(cat.Describe()) }
class Animal { final String name; final String species; Animal(this.name, this.species); // shorthand constructor String describe() => '$name is a $species'; } void main() { final cat = Animal('Whiskers', 'cat'); print(cat.describe()); }
Go has no classes — data is held in structs and methods are defined separately with a receiver. Dart is a pure OOP language: fields and methods are declared inside the class block, and all values are objects (there are no primitive types separate from the type system). Dart's this.name initializing constructor shorthand eliminates the common boilerplate of assigning each parameter to a field.
Constructors
package main import "fmt" type Point struct { X, Y float64 } func NewPoint(x, y float64) *Point { return &Point{X: x, Y: y} } func NewOrigin() *Point { return &Point{} } func main() { point := NewPoint(3.0, 4.0) origin := NewOrigin() fmt.Println(point, origin) }
class Point { final double x; final double y; // Default constructor Point(this.x, this.y); // Named constructor — no "New" prefix convention needed Point.origin() : x = 0, y = 0; // Named constructor from a map Point.fromMap(Map<String, double> coordinates) : x = coordinates['x'] ?? 0, y = coordinates['y'] ?? 0; @override String toString() => 'Point($x, $y)'; } void main() { final point = Point(3.0, 4.0); final origin = Point.origin(); final mapped = Point.fromMap({'x': 1.5, 'y': 2.5}); print(point); print(origin); print(mapped); }
Go uses constructor functions by convention (NewPoint(), NewOrigin()). Dart builds multiple constructors directly into the class using named constructors (Point.origin(), Point.fromMap()). The initializer list (: x = 0, y = 0) sets fields before the constructor body runs, which is required for final fields. The @override annotation on toString() has no Go equivalent — Go has no inheritance and no interface annotations.
Inheritance
package main import "fmt" // Go uses composition (embedding) instead of inheritance type Shape struct { color string } func (shape Shape) Describe() string { return "a " + shape.color + " shape" } type Circle struct { Shape // embedded — Circle "inherits" Describe() radius float64 } func main() { circle := Circle{Shape: Shape{color: "red"}, radius: 5.0} fmt.Println(circle.Describe()) }
class Shape { final String color; Shape(this.color); String describe() => 'a $color shape'; } class Circle extends Shape { final double radius; Circle({required this.radius, required String color}) : super(color); @override String describe() => 'a $color circle with radius $radius'; double get area => 3.14159 * radius * radius; } void main() { final circle = Circle(radius: 5.0, color: 'red'); print(circle.describe()); print(circle.area); Shape shape = circle; // Circle is-a Shape print(shape.describe()); // polymorphic dispatch }
Go favors composition via struct embedding — a struct that embeds another type "inherits" its methods, but there is no subtype relationship and no polymorphism in the OOP sense. Dart uses classical inheritance with extends: a Circle is a subtype of Shape, method overriding is declared with @override, and a Shape variable can hold any subclass. This enables polymorphic dispatch, which Go achieves only through interfaces.
Getters and setters
package main import "fmt" type Temperature struct { celsius float64 } func (temp Temperature) Fahrenheit() float64 { return temp.celsius*9/5 + 32 } func (temp *Temperature) SetFahrenheit(fahrenheit float64) { temp.celsius = (fahrenheit - 32) * 5 / 9 } func main() { temp := Temperature{celsius: 100} fmt.Println(temp.Fahrenheit()) temp.SetFahrenheit(32) fmt.Println(temp.celsius) }
class Temperature { double celsius; Temperature(this.celsius); // Getter — called as a property, not a method double get fahrenheit => celsius * 9 / 5 + 32; // Setter — assigned as a property set fahrenheit(double value) { celsius = (value - 32) * 5 / 9; } } void main() { final temp = Temperature(100); print(temp.fahrenheit); // getter: no () temp.fahrenheit = 32; // setter print(temp.celsius); }
Go uses conventional methods (Fahrenheit() with a call site of temp.Fahrenheit()) for computed properties. Dart has first-class get and set keywords that turn methods into properties: the getter is invoked as temp.fahrenheit (no parentheses), and the setter is invoked as temp.fahrenheit = 32. This allows computed properties to be interchangeable with stored fields at the call site.
Static members
package main import "fmt" type Counter struct { value int } var totalCreated int // package-level variable (no struct-level static) func NewCounter() *Counter { totalCreated++ return &Counter{} } func main() { c1 := NewCounter() c2 := NewCounter() fmt.Println(c1, c2, totalCreated) }
class Counter { int value = 0; static int totalCreated = 0; // shared across all instances Counter() { totalCreated++; } static Counter create() { return Counter(); } void increment() => value++; } void main() { final counter1 = Counter(); final counter2 = Counter.create(); print(Counter.totalCreated); // 2 — accessed on the class, not an instance counter1.increment(); print(counter1.value); }
Go has no struct-level static members; class-wide shared state is expressed as package-level variables. Dart has static fields and methods that belong to the class, not any instance. Static members are accessed via the class name (Counter.totalCreated), not via an instance — just like Go accesses package-level variables via the package name (package.Variable).
Interfaces & Mixins
Interfaces (abstract classes)
package main import ( "fmt" "math" ) // Go interface — implicit: any type with these methods satisfies it type Shape interface { Area() float64 Perimeter() float64 } type Circle struct{ radius float64 } func (circle Circle) Area() float64 { return math.Pi * circle.radius * circle.radius } func (circle Circle) Perimeter() float64 { return 2 * math.Pi * circle.radius } func printInfo(shape Shape) { fmt.Printf("area=%.2f perimeter=%.2f\n", shape.Area(), shape.Perimeter()) } func main() { printInfo(Circle{radius: 5}) }
import 'dart:math'; abstract class Shape { double area(); // no body = abstract method, must be overridden double perimeter(); } class Circle implements Shape { final double radius; Circle(this.radius); @override double area() => pi * radius * radius; @override double perimeter() => 2 * pi * radius; } void printInfo(Shape shape) { print('area=${shape.area().toStringAsFixed(2)} ' 'perimeter=${shape.perimeter().toStringAsFixed(2)}'); } void main() { printInfo(Circle(5)); }
Go interfaces are implicitly satisfied — a type implements an interface just by having the right methods, with no declaration at the definition site. Dart requires an explicit implements Shape declaration. Dart uses abstract class to define interfaces (Dart 3 also adds the interface keyword for pure interface types). Unlike Go, Dart's abstract class can contain concrete methods with a body alongside abstract ones.
Implementing multiple interfaces
package main import "fmt" type Reader interface{ Read() string } type Writer interface{ Write(text string) } type ReadWriter interface { Reader Writer } type Buffer struct{ data string } func (buffer *Buffer) Read() string { return buffer.data } func (buffer *Buffer) Write(text string) { buffer.data += text } func main() { var readWriter ReadWriter = &Buffer{} readWriter.Write("hello ") readWriter.Write("world") fmt.Println(readWriter.Read()) }
abstract class Reader { String read(); } abstract class Writer { void write(String text); } // Declare a combining interface — the Dart equivalent of Go's embedded interface abstract class ReadWriter implements Reader, Writer {} class Buffer implements ReadWriter { String _data = ''; @override String read() => _data; @override void write(String text) => _data += text; } void main() { ReadWriter buffer = Buffer(); buffer.write('hello '); buffer.write('world'); print(buffer.read()); }
Both Go and Dart support implementing multiple interfaces. Go embeds interfaces to compose them: type ReadWriter interface { Reader; Writer }. Dart does the same with an abstract class: abstract class ReadWriter implements Reader, Writer {}. A concrete class then implements the combined interface. The key difference is that Go's interface is implicit (any type with the right methods satisfies it), while Dart requires the implements declaration.
Mixins
package main import "fmt" // Go: compose behavior via embedding multiple structs type Logger struct{} func (logger Logger) Log(message string) { fmt.Println("[LOG]", message) } type Timestamper struct{} func (timestamper Timestamper) Now() string { return "2026-01-01" } type Service struct { Logger Timestamper name string } func main() { service := Service{name: "api"} service.Log(service.name + " started at " + service.Now()) }
// Mixins share reusable behavior without inheritance mixin Logger { void log(String message) => print('[LOG] $message'); } mixin Timestamper { String now() => '2026-01-01'; } class Service with Logger, Timestamper { final String name; Service(this.name); void start() => log('$name started at ${now()}'); } void main() { final service = Service('api'); service.start(); service.log('direct log call'); }
Go uses struct embedding to compose behavior — embedding a struct adds its fields and methods to the outer struct. Dart has a first-class mixin keyword for the same purpose: a mixin defines reusable methods that a class can with in. Mixins are distinct from inheritance (extends) because they compose horizontally rather than forming a subtype relationship. A class can mix in multiple mixins (with Logger, Timestamper).
Type assertion / type checking
package main import "fmt" type Animal interface { Sound() string } type Dog struct{ name string } type Cat struct{ name string } func (dog Dog) Sound() string { return "woof" } func (cat Cat) Sound() string { return "meow" } func identify(animal Animal) { switch concrete := animal.(type) { case Dog: fmt.Println("Dog:", concrete.name) case Cat: fmt.Println("Cat:", concrete.name) } } func main() { identify(Dog{name: "Rex"}) identify(Cat{name: "Mittens"}) }
abstract class Animal { String sound(); } class Dog extends Animal { final String name; Dog(this.name); @override String sound() => 'woof'; } class Cat extends Animal { final String name; Cat(this.name); @override String sound() => 'meow'; } void identify(Animal animal) { if (animal is Dog) { print('Dog: ${animal.name}'); // animal promoted to Dog here } else if (animal is Cat) { print('Cat: ${animal.name}'); } } void main() { identify(Dog('Rex')); identify(Cat('Mittens')); }
Go uses a type switch (switch x := value.(type)) to branch on the concrete type behind an interface. Dart uses is and as: the is operator checks the type and automatically promotes the variable to that type within the branch (no explicit cast needed). Dart 3 also supports sealed class hierarchies with exhaustive switch expressions, giving compile-time guarantees that all cases are handled.
Generics
Generic functions
package main import "fmt" func first[T any](slice []T) T { return slice[0] } func contains[T comparable](slice []T, target T) bool { for _, item := range slice { if item == target { return true } } return false } func main() { numbers := []int{10, 20, 30} words := []string{"go", "dart", "rust"} fmt.Println(first(numbers)) fmt.Println(first(words)) fmt.Println(contains(words, "dart")) }
T first<T>(List<T> items) => items.first; bool contains<T>(List<T> items, T target) { return items.contains(target); } void main() { final numbers = [10, 20, 30]; final words = ['go', 'dart', 'rust']; print(first(numbers)); print(first(words)); print(contains(words, 'dart')); }
Both languages have generics, but the syntax differs: Go uses square brackets ([T any], [T comparable]), while Dart uses angle brackets (<T>). Type parameters in both languages are inferred at call sites when possible. Dart's constraint system is simpler than Go's — Dart relies on extending a bound class (<T extends Comparable<T>>) rather than Go's interface-based type sets.
Generic classes
package main import "fmt" type Stack[T any] struct { items []T } func (stack *Stack[T]) Push(item T) { stack.items = append(stack.items, item) } func (stack *Stack[T]) Pop() (T, bool) { if len(stack.items) == 0 { var zero T return zero, false } top := stack.items[len(stack.items)-1] stack.items = stack.items[:len(stack.items)-1] return top, true } func main() { stack := &Stack[int]{} stack.Push(1) stack.Push(2) stack.Push(3) value, ok := stack.Pop() fmt.Println(value, ok) }
class Stack<T> { final List<T> _items = []; void push(T item) => _items.add(item); T pop() { if (_items.isEmpty) throw StateError('Stack is empty'); return _items.removeLast(); } bool get isEmpty => _items.isEmpty; int get length => _items.length; } void main() { final stack = Stack<int>(); stack.push(1); stack.push(2); stack.push(3); print(stack.pop()); // 3 print(stack.length); // 2 }
Generic classes use the same angle-bracket syntax as generic functions. One conceptual difference: where Go's generic Pop uses a second bool return to signal "empty stack", Dart throws an exception (StateError). There is no zero-value trick in Dart for an empty generic (var zero T), because Dart's null safety means you would need T? and return null instead.
Type bounds
package main import ( "cmp" "fmt" ) func maxValue[T cmp.Ordered](first, second T) T { if first > second { return first } return second } func main() { fmt.Println(maxValue(3, 7)) fmt.Println(maxValue(3.14, 2.72)) fmt.Println(maxValue("apple", "banana")) }
// Dart bounds use extends — T must be a subtype of Comparable<T> T maxValue<T extends Comparable<T>>(T first, T second) { return first.compareTo(second) >= 0 ? first : second; } void main() { print(maxValue(3, 7)); print(maxValue(3.14, 2.72)); print(maxValue('apple', 'banana')); }
Go constrains type parameters with interfaces as type sets (constraints.Ordered allows >). Dart uses extends to constrain a type parameter to a class or interface: T extends Comparable<T> requires the type to implement the compareTo() method. The result is that Dart calls a method (first.compareTo(second)) where Go uses an operator (first > second).
Error Handling
Error returns vs exceptions
package main import ( "errors" "fmt" "strconv" ) func parseAge(input string) (int, error) { age, err := strconv.Atoi(input) if err != nil { return 0, fmt.Errorf("invalid age %q: %w", input, err) } if age < 0 || age > 150 { return 0, errors.New("age out of range") } return age, nil } func main() { age, err := parseAge("25") if err != nil { fmt.Println("error:", err) } else { fmt.Println("age:", age) } _, err = parseAge("abc") if err != nil { fmt.Println("error:", err) } }
int parseAge(String input) { final age = int.parse(input); // throws FormatException if not a number if (age < 0 || age > 150) { throw RangeError.range(age, 0, 150, 'age'); } return age; } void main() { try { print('age: ${parseAge('25')}'); print('age: ${parseAge('abc')}'); // throws FormatException } on FormatException catch (exception) { print('parse error: $exception'); } on RangeError catch (exception) { print('range error: $exception'); } }
This is the most fundamental difference between Go and Dart. Go requires explicit error propagation — every fallible function returns (value, error) and the caller must check the error every time. Dart uses exceptions: a function either returns a value or throws; the caller wraps a block with try/catch to handle failures. The Go style makes all error paths visible at every call site; the Dart style concentrates error handling at a chosen boundary.
try / on / catch / finally
package main import "fmt" func riskyOperation() (string, error) { return "", fmt.Errorf("something went wrong") } func main() { result, err := riskyOperation() if err != nil { fmt.Println("caught:", err) result = "default" } defer fmt.Println("cleanup runs last") // Go's finally equivalent fmt.Println("result:", result) }
void riskyOperation() { throw Exception('something went wrong'); } void main() { try { riskyOperation(); } on Exception catch (exception) { print('caught: $exception'); } catch (exception, stackTrace) { // catch without "on" catches everything print('unexpected: $exception'); print(stackTrace); } finally { print('cleanup always runs'); } }
Dart's on ExceptionType catch (exception) syntax catches a specific type, similar to how Go checks errors.Is(err, target) or a type assertion. The bare catch (exception) with no on clause catches any type. The finally block always runs, mirroring Go's defer — but defer runs deferred functions in LIFO order at function exit, while finally runs only within its try block.
Custom exceptions
package main import ( "errors" "fmt" ) type ValidationError struct { Field string Message string } func (validationError *ValidationError) Error() string { return fmt.Sprintf("validation error on %s: %s", validationError.Field, validationError.Message) } func validate(email string) error { if len(email) == 0 { return &ValidationError{Field: "email", Message: "cannot be empty"} } return nil } func main() { err := validate("") var validationError *ValidationError if errors.As(err, &validationError) { fmt.Println(validationError.Field, validationError.Message) } }
class ValidationException implements Exception { final String field; final String message; const ValidationException({required this.field, required this.message}); @override String toString() => 'ValidationException on $field: $message'; } void validate(String email) { if (email.isEmpty) { throw ValidationException(field: 'email', message: 'cannot be empty'); } } void main() { try { validate(''); } on ValidationException catch (exception) { print(exception.field); print(exception.message); print(exception); } }
Custom error types in Go implement the error interface by providing an Error() string method. Custom exceptions in Dart implement the Exception interface (or extend Error for programming errors) and override toString(). The Dart on ValidationException clause replaces Go's errors.As(err, &target) pattern for type-checking errors.
Error propagation
package main import ( "errors" "fmt" ) var ErrNotFound = errors.New("not found") func fetchRecord(identifier int) (string, error) { if identifier != 42 { return "", fmt.Errorf("fetchRecord(%d): %w", identifier, ErrNotFound) } return "record-42", nil } func processRecord(identifier int) (string, error) { record, err := fetchRecord(identifier) if err != nil { return "", fmt.Errorf("processRecord: %w", err) } return "processed:" + record, nil } func main() { result, err := processRecord(99) if err != nil { fmt.Println(err) fmt.Println(errors.Is(err, ErrNotFound)) } else { fmt.Println(result) } }
class NotFoundException implements Exception { final String message; const NotFoundException(this.message); @override String toString() => 'NotFoundException: $message'; } String fetchRecord(int identifier) { if (identifier != 42) { throw NotFoundException('record $identifier not found'); } return 'record-42'; } String processRecord(int identifier) { // Exceptions propagate automatically — no wrapping needed return 'processed:${fetchRecord(identifier)}'; } void main() { try { print(processRecord(99)); } on NotFoundException catch (exception) { print(exception); print('is not-found: true'); } print(processRecord(42)); }
In Go, every function in a call chain must explicitly pass errors up with fmt.Errorf(...%w, err), adding context at each level. In Dart, exceptions propagate automatically up the call stack until a catch clause handles them — intermediate functions need no error-handling code at all. The Go approach makes all error paths explicit in the source; the Dart approach makes the happy path cleaner at the cost of sometimes surprising exception propagation.
Async & Futures
Goroutines vs Futures
package main import ( "fmt" "time" ) func slowGreeting(name string, done chan<- string) { time.Sleep(0) // simulated work done <- "Hello, " + name + "!" } func main() { done := make(chan string, 1) go slowGreeting("Dart", done) // launch goroutine greeting := <-done // wait for result fmt.Println(greeting) }
Future<String> slowGreeting(String name) async { // await simulates async work (like a network call) await Future.delayed(Duration(milliseconds: 0)); return 'Hello, $name!'; } Future<void> main() async { // await pauses main until the Future completes final greeting = await slowGreeting('Go'); print(greeting); // Run two futures concurrently with Future.wait: final results = await Future.wait([ slowGreeting('Go'), slowGreeting('Dart'), ]); results.forEach(print); }
Go's concurrency model uses goroutines (lightweight threads managed by the Go runtime) and channels (typed message queues). Dart uses a cooperative async model: a Future<T> is a promise of a single value, and async/await suspends a function until the value is available. Dart's event loop is single-threaded — there is no true parallelism within a single Isolate. Use Future.wait() to run multiple futures concurrently (interleaved on the event loop, not in parallel).
async / await
package main import ( "fmt" "strconv" ) func fetchPrice(product string) (float64, error) { prices := map[string]float64{"apple": 1.50, "banana": 0.75} price, exists := prices[product] if !exists { return 0, fmt.Errorf("product %q not found", product) } return price, nil } func totalCost(products []string) (float64, error) { total := 0.0 for _, product := range products { price, err := fetchPrice(product) if err != nil { return 0, err } total += price } return total, nil } func main() { total, err := totalCost([]string{"apple", "banana"}) if err != nil { fmt.Println("error:", err) return } fmt.Println("total:", strconv.FormatFloat(total, 'f', 2, 64)) }
Future<double> fetchPrice(String product) async { final prices = {'apple': 1.50, 'banana': 0.75}; if (!prices.containsKey(product)) { throw Exception('product "$product" not found'); } return prices[product]!; } Future<double> totalCost(List<String> products) async { double total = 0; for (final product in products) { total += await fetchPrice(product); // await each in sequence } return total; } Future<void> main() async { try { final total = await totalCost(['apple', 'banana']); print('total: ${total.toStringAsFixed(2)}'); } catch (exception) { print('error: $exception'); } }
Dart's async/await makes asynchronous code read like sequential synchronous code. An async function always returns a Future; await suspends the function until the Future completes. Error handling in async code uses the same try/catch as synchronous code — no special goroutine or channel mechanics. The pattern is closely analogous to JavaScript's async/await, which is no coincidence — Dart and JavaScript share the same event-loop concurrency model.
Streams vs channels
package main import "fmt" func countdown(from int) <-chan int { channel := make(chan int) go func() { for count := from; count >= 0; count-- { channel <- count } close(channel) }() return channel } func main() { for value := range countdown(5) { fmt.Print(value, " ") } fmt.Println() }
Stream<int> countdown(int from) async* { for (int count = from; count >= 0; count--) { yield count; // emit one value at a time } } Future<void> main() async { // Collect stream values into a list, then print final values = await countdown(5).toList(); print(values.join(' ')); // Streams support functional-style transforms: final doubled = await countdown(5).map((count) => count * 2).toList(); print(doubled.join(' ')); }
Go channels are typed communication pipes between goroutines: a goroutine sends on one end, another receives on the other, and the channel buffers values. Dart Streams are asynchronous sequences of values: an async* function yields values one at a time, and the consumer iterates with await for. Streams support the same functional transforms as lists (map(), where(), take()), making them composable pipelines. Unlike Go channels, Streams are single-subscription by default.
Isolates (true parallelism)
package main import ( "fmt" "sync" ) func heavyComputation(input int, waitGroup *sync.WaitGroup, results chan<- int) { defer waitGroup.Done() total := 0 for i := 0; i < input*1000000; i++ { total += i } results <- total } func main() { var waitGroup sync.WaitGroup results := make(chan int, 2) waitGroup.Add(2) go heavyComputation(100, &waitGroup, results) go heavyComputation(200, &waitGroup, results) waitGroup.Wait() close(results) for result := range results { fmt.Println(result) } }
// Isolates run in parallel with separate memory — no shared state // (Requires dart:isolate, which is not available in all environments) // // import 'dart:isolate'; // // void heavyComputation(SendPort sendPort) { // int total = 0; // for (int i = 0; i < 100000000; i++) total += i; // sendPort.send(total); // } // // Future<void> main() async { // final receivePort = ReceivePort(); // await Isolate.spawn(heavyComputation, receivePort.sendPort); // final result = await receivePort.first; // print(result); // } // Use compute() in Flutter, or Isolate.run() in Dart 2.19+ (simpler API): void main() { print('Isolate parallelism requires dart:isolate or Flutter compute().'); print('For async concurrency (I/O-bound), use Future.wait() instead.'); }
Go goroutines share memory with a lock-based synchronization model (sync.WaitGroup, sync.Mutex). Dart Isolates are fully isolated — they have no shared memory and communicate only by passing messages through ports. This eliminates data races by design, at the cost of copying data between Isolates. For I/O-bound work (most async code), async/await on a single Isolate is sufficient. Use Isolate.run() for CPU-bound work that would otherwise block the event loop.