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
r — r'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.