Basics & Syntax
Hello, World
print("Hello, World!") fmt.Println("Hello, World!") Go programs are a
package main with a func main(); the examples on this page show just the body, and printing comes from the fmt package rather than a built-in. Two habits from the start: the compiler rejects an unused import or an unused local variable (not a warning — an error), and gofmt settles every formatting question, so nobody argues about braces.let and var become := and var
let fixed = 1
var counter = 0
counter += 1
let explicit: Int64 = 42
let name: String = "Ada"
print(fixed + counter, explicit, name) // := declares and infers. It is the everyday form, and it works only inside
// a function.
counter := 0
counter += 1
// var, with or without a type.
var explicit int64 = 42
var name = "Ada"
// const is the only immutable binding — and it works only for compile-time
// values (numbers, strings, booleans). There is NO 'let' for a computed value:
// an ordinary variable is what you get, and nothing stops it being reassigned.
const fixed = 1
fmt.Println(fixed+counter, explicit, name) The mutability default is inverted, and there is no way to get it back. Go's
const is not Swift's let — it only holds compile-time constants, so a value computed at run time (the result of a function, a struct, a slice) can only be a var, and nothing prevents anyone from reassigning it. Immutability in Go is a convention you maintain by hand, which is a genuine step down from Swift. What you get in exchange is :=, which is terse enough that the type annotation almost never appears.Functions return several values
// Argument labels, defaults, and a single return value (or a tuple).
func divide(_ numerator: Int, by denominator: Int) -> (quotient: Int, remainder: Int) {
(numerator / denominator, numerator % denominator)
}
func greet(name: String, salutation: String = "Hello") -> String {
"\(salutation), \(name)!"
}
let result = divide(17, by: 5)
print(result.quotient, result.remainder)
print(greet(name: "Ada")) // Multiple return values are a language feature, not a tuple type — and they
// can be named, which documents them.
func divide(numerator, denominator int) (quotient, remainder int) {
return numerator / denominator, numerator % denominator
}
// No argument labels and NO DEFAULT ARGUMENTS. You write the other function.
func greet(name string) string {
return greetWith(name, "Hello")
}
func greetWith(name, salutation string) string {
return salutation + ", " + name + "!"
}
quotient, remainder := divide(17, 5)
fmt.Println(quotient, remainder)
fmt.Println(greet("Ada"))
// _ discards a return value you do not want. The compiler INSISTS: an unused
// variable is a compile error.
onlyQuotient, _ := divide(17, 5)
fmt.Println(onlyQuotient) The type goes after the name, and multiple return values are built into the language rather than being a tuple — which matters more than it sounds, because that is the mechanism the entire error-handling story is built on (
value, err := doThing()). Two Swift conveniences are gone: no argument labels, so a call site is bare positional arguments, and no default arguments, so you write a second function. And note _: Go forces you to explicitly discard a return value you are ignoring, because an unused variable will not compile.No Optionals: Zero Values
There are no optionals — there is the zero value
The absence you will feel on the first day. Go has no
Optional, no ?, no if let. Every type has a zero value, and that is what "nothing" looks like.struct User {
var name: String
var age: Int
}
// An optional says "this may be absent", and the compiler makes you deal with it.
func findUser(_ id: Int) -> User? {
id == 1 ? User(name: "Ada", age: 36) : nil
}
if let user = findUser(1) {
print(user.name)
}
print(findUser(99)?.name ?? "(nobody)")
// "empty" and "absent" are different types, and cannot be confused.
let noName: String? = nil
let emptyName: String? = ""
print(noName == emptyName) // false type User struct {
Name string
Age int
}
// The convention: return the value AND a boolean saying whether it is real.
// This "comma ok" idiom is Go's optional, and it is a convention, not a type.
func findUser(id int) (User, bool) {
if id == 1 {
return User{Name: "Ada", Age: 36}, true
}
return User{}, false // the ZERO value: Name is "", Age is 0
}
if user, ok := findUser(1); ok {
fmt.Println(user.Name)
}
user, ok := findUser(99)
if !ok {
fmt.Println("(nobody)")
}
fmt.Printf("the zero User is %+v\n", user) // NOT nil — an empty struct
// Every type has one: 0, "", false, nil for pointers/slices/maps/interfaces.
var number int
var text string
var items []int
fmt.Printf("%d %q %v %v\n", number, text, items == nil, len(items)) A Go value is never "absent" — it is zero. That has one genuinely nice consequence (a zero
struct is usable, so var buffer bytes.Buffer is ready to write to with no initialization) and one real cost: the type system cannot distinguish "empty" from "missing". An empty string may mean the user has no name or that you forgot to load it, and nothing will tell you which. The idioms that fill the gap are the comma ok pattern above (value, ok := …) and pointers (*User, where nil means absent) — and a nil pointer dereference is a runtime panic, exactly the class of bug Swift's optionals were introduced to eliminate.Pointers, and the nil that comes with them
class Node {
var value: Int
var next: Node? // a reference, and it may be nil — the type says so
init(value: Int) { self.value = value }
}
let first = Node(value: 1)
first.next = Node(value: 2)
// Optional chaining: safe by construction.
print(first.next?.value ?? -1)
print(first.next?.next?.value ?? -1)
// inout, for passing a value type by reference.
func increment(_ number: inout Int) { number += 1 }
var counter = 0
increment(&counter)
print(counter) type Node struct {
Value int
Next *Node // a pointer. nil means "no next" — and nothing enforces a check.
}
first := &Node{Value: 1}
first.Next = &Node{Value: 2}
// No optional chaining. You check, by hand, at every hop.
if first.Next != nil {
fmt.Println(first.Next.Value)
}
if first.Next != nil && first.Next.Next != nil {
fmt.Println(first.Next.Next.Value)
} else {
fmt.Println(-1)
}
// first.Next.Next.Value ← a nil dereference: PANIC at run time
// Go has no inout: you pass a pointer, and the & / * are explicit.
func increment(number *int) { *number++ }
counter := 0
increment(&counter)
fmt.Println(counter)
// But Go's pointers are safe in the ways that matter: no arithmetic, no
// dangling pointers (the GC keeps the target alive), no manual free.
fmt.Println(*first.Next) Pointers are back, and they are the price of not having optionals:
*Node is how you say "maybe there is a next node", and nil is how you say there is not. The compiler will not make you check, so a nil dereference is a runtime panic — the bug Swift eliminated with optionals, alive and well. Go's pointers are at least memory-safe (no arithmetic, no dangling references, garbage-collected), and there is no inout: you pass &value and write through *pointer. The other thing to know is that method receivers can be pointers, and that is what makes a method able to mutate its receiver at all.Errors Are Values
throws becomes an extra return value
The single most Go thing about Go. An error is not thrown, caught, or propagated for you — it is a value returned alongside the result, and checking it is your job on every single call.
enum ParseError: Error {
case notANumber(String)
case notPositive(Int)
}
func parsePositive(_ text: String) throws -> Int {
guard let number = Int(text) else {
throw ParseError.notANumber(text)
}
guard number > 0 else {
throw ParseError.notPositive(number)
}
return number
}
// 'try' marks the call; the error propagates up on its own.
func doubled(_ text: String) throws -> Int {
try parsePositive(text) * 2
}
do {
print(try doubled("21"))
print(try doubled("-1"))
} catch ParseError.notPositive(let number) {
print("not positive: \(number)")
} catch {
print("failed: \(error)")
} // Sentinel errors, compared with errors.Is.
var ErrNotPositive = errors.New("not positive")
func parsePositive(text string) (int, error) {
number, err := strconv.Atoi(text)
if err != nil {
// %w WRAPS the underlying error, so a caller can still unwrap it.
return 0, fmt.Errorf("parsing %q: %w", text, err)
}
if number <= 0 {
return 0, fmt.Errorf("%d: %w", number, ErrNotPositive)
}
return number, nil
}
// There is no 'try' and no automatic propagation: every call site checks, and
// passes the error up by hand. This is the famous verbosity, and it is the point.
func doubled(text string) (int, error) {
number, err := parsePositive(text)
if err != nil {
return 0, err
}
return number * 2, nil
}
value, err := doubled("21")
if err != nil {
fmt.Println("failed:", err)
} else {
fmt.Println(value)
}
if _, err := doubled("-1"); errors.Is(err, ErrNotPositive) {
fmt.Println("not positive:", err)
}
if _, err := doubled("nope"); err != nil {
fmt.Println("failed:", err)
} An error is an ordinary value of the
error interface, so it appears in the signature, can be stored in a struct, sent down a channel, or compared — and it must be checked at every call, because nothing propagates it for you. That is why roughly one line in three of real Go is if err != nil, and Go's position is that this is a feature: the error path is as visible as the happy path, and it is impossible to be surprised by an error escaping from a function you did not know could fail. Swift's throws makes the happy path far more readable and hides the failure path completely. The pieces to learn are %w (wraps an error so the chain is preserved), errors.Is (compares against a sentinel), and errors.As (extracts a concrete error type — the closest thing to a catch pattern).defer is the same; panic is not an exception
func process() {
print("opening")
defer { print("closing") } // runs on every exit path
print("working")
}
process()
// fatalError is the unrecoverable exit; it cannot be caught.
func mustBePositive(_ number: Int) -> Int {
precondition(number > 0, "must be positive")
return number
}
print(mustBePositive(5)) func process() {
fmt.Println("opening")
defer fmt.Println("closing") // runs when the FUNCTION returns — not the block
fmt.Println("working")
}
process()
// panic is fatalError: for a bug, not for expected failure. recover() can stop
// it — but doing so routinely is an anti-pattern, and libraries that panic
// across an API boundary are considered badly behaved.
func mustBePositive(number int) (result int, err error) {
defer func() {
if recovered := recover(); recovered != nil {
err = fmt.Errorf("recovered: %v", recovered)
}
}()
if number <= 0 {
panic("must be positive")
}
return number, nil
}
fmt.Println(mustBePositive(5))
fmt.Println(mustBePositive(-1)) defer is one of the few things that ports with no adjustment at all — same idea, same purpose, and the deferred calls run last-in-first-out. One difference to burn in: Swift's defer fires at the end of the enclosing scope, while Go's fires at the end of the enclosing function, so a defer inside a loop body does not run until the whole function returns (the classic Go leak: deferring a Close inside a loop over ten thousand files). panic is fatalError, and although recover exists, using it as an exception handler is un-Go — errors are values, and a panic means a bug.Structs & Methods
Structs and methods, with a receiver
struct Point {
var x: Int
var y: Int
// A computed property.
var magnitude: Double {
Double(x * x + y * y).squareRoot()
}
func offset(by delta: Int) -> Point {
Point(x: x + delta, y: y + delta)
}
// 'mutating' to change self.
mutating func moveRight() {
x += 1
}
}
var point = Point(x: 3, y: 4)
print(point.magnitude)
print(point.offset(by: 1))
point.moveRight()
print(point) type Point struct {
X, Y int
}
// Methods are declared OUTSIDE the type, with a receiver in front of the name.
// A VALUE receiver gets a copy — it cannot mutate the original.
func (point Point) Magnitude() float64 {
return math.Sqrt(float64(point.X*point.X + point.Y*point.Y))
}
func (point Point) Offset(delta int) Point {
return Point{X: point.X + delta, Y: point.Y + delta}
}
// A POINTER receiver can. This is Swift's 'mutating', spelled out.
func (point *Point) MoveRight() {
point.X++
}
point := Point{X: 3, Y: 4}
fmt.Println(point.Magnitude()) // there are no computed properties: it is a method
fmt.Println(point.Offset(1))
point.MoveRight() // Go takes the address for you: (&point).MoveRight()
fmt.Println(point)
// Capitalization IS the access control: exported means capitalized.
// X and Y are visible outside the package; a lowercase field would not be.
fmt.Printf("%+v\n", point) Structs are value types in both languages, and Go's pointer receiver (
func (point *Point) MoveRight()) is precisely Swift's mutating — Go just makes the mechanism visible instead of the intent. Three things to internalize. Methods live outside the type, so a type's behavior can be spread across a file (or added to any type you define). There are no computed properties, so point.magnitude becomes point.Magnitude(). And access control is capitalization: an uppercase name is exported from its package, a lowercase one is not — there is no private, no internal, and the unit of privacy is the package, not the type.No inheritance: embedding instead
class Animal {
let name: String
init(name: String) { self.name = name }
func speak() -> String { "..." }
func introduce() -> String { "\(name) says \(speak())" }
}
class Dog: Animal {
override func speak() -> String { "Woof" }
}
print(Dog(name: "Rex").introduce()) type Animal struct {
Name string
}
func (animal Animal) Speak() string { return "..." }
type Dog struct {
Animal // EMBEDDED: no field name, so Animal's fields and methods are promoted
}
// Dog "overrides" Speak simply by defining its own.
func (dog Dog) Speak() string { return "Woof" }
dog := Dog{Animal: Animal{Name: "Rex"}}
fmt.Println(dog.Name) // promoted from the embedded Animal
fmt.Println(dog.Speak()) // Dog's own
fmt.Println(dog.Animal.Speak()) // the embedded one is still reachable
// The trap: embedding is NOT inheritance. A method PROMOTED from Animal calls
// Animal.Speak, not Dog.Speak — there is no dynamic dispatch back to the outer
// type, so this prints "Rex says ...", NOT "Rex says Woof".
func (animal Animal) Introduce() string {
return animal.Name + " says " + animal.Speak()
}
fmt.Println(dog.Introduce()) Go has no inheritance, and embedding is not a substitute for it — read the last line of the right column carefully, because it is the mistake every newcomer makes. Embedding promotes the inner type's fields and methods to the outer type, which looks like subclassing, but there is no dynamic dispatch back down:
Animal.Introduce calls Animal.Speak, and it has no idea Dog exists. Polymorphism in Go comes from interfaces and nothing else. If you need the template-method pattern from the left column, the Go answer is to take the varying behavior as an interface value rather than to embed a base struct.Interfaces
Conformance is implicit
The deepest philosophical difference between the two type systems. A Swift type declares which protocols it conforms to; a Go type conforms to an interface simply by having the right methods — and never mentions it.
protocol Describable {
func describe() -> String
}
// Conformance is DECLARED. The type must know about the protocol.
struct Dog: Describable {
let name: String
func describe() -> String { "\(name) the dog" }
}
struct Robot: Describable {
func describe() -> String { "BEEP" }
}
func announce(_ value: any Describable) {
print(value.describe())
}
announce(Dog(name: "Rex"))
announce(Robot())
// You cannot retroactively conform someone else's type without an extension —
// and you cannot conform a type you did not define to a protocol you did not
// define at all. type Describable interface {
Describe() string
}
type Dog struct{ Name string }
type Robot struct{}
// NOTHING here mentions Describable. The types satisfy it because they happen
// to have the method — structural, implicit, checked at compile time.
func (dog Dog) Describe() string { return dog.Name + " the dog" }
func (robot Robot) Describe() string { return "BEEP" }
func announce(value Describable) {
fmt.Println(value.Describe())
}
announce(Dog{Name: "Rex"})
announce(Robot{})
// So an interface can be defined AFTER the types, by the CONSUMER — which is
// the whole Go design philosophy: "accept interfaces, return structs", and
// define the interface where it is USED, not where the type is declared.
type Namer interface{ Describe() string }
var things = []Namer{Dog{Name: "Rex"}, Robot{}}
for _, thing := range things {
fmt.Println(thing.Describe())
}
// A type switch recovers the concrete type — Swift's 'is' / 'as?'.
for _, thing := range things {
switch concrete := thing.(type) {
case Dog:
fmt.Println("a dog named", concrete.Name)
case Robot:
fmt.Println("a robot")
}
} Implicit conformance inverts who owns the abstraction. In Swift, the type declares what it conforms to, so an interface must exist before a type can satisfy it. In Go, the consumer defines the interface it needs, after the fact, and any type with matching methods satisfies it automatically — including types from a library you do not control. That is why idiomatic Go interfaces are tiny (
io.Reader has one method) and why the house rule is "accept interfaces, return structs". The cost is discoverability: nothing in a type's declaration tells you what it satisfies, so finding "all the things that implement this" is a job for your editor rather than the language. The value.(type) switch is the escape hatch back to the concrete type, and it is Swift's as?.any, and the nil interface trap
// Swift's Any, and a nil that is always just nil.
let values: [Any] = [1, "two", 3.0, nil as Int?]
for value in values {
switch value {
case let number as Int:
print("int \(number)")
case let text as String:
print("string \(text)")
default:
print("something else: \(value)")
}
}
let maybe: Int? = nil
print(maybe == nil) // true. Always. There is no subtlety here. // interface{} (spelled 'any' since 1.18) holds anything.
values := []any{1, "two", 3.0, nil}
for _, value := range values {
switch concrete := value.(type) {
case int:
fmt.Println("int", concrete)
case string:
fmt.Println("string", concrete)
default:
fmt.Println("something else:", concrete)
}
}
// THE classic Go bug. An interface value is (type, value). A nil *pointer*
// stored in an interface makes the interface NON-nil, because its type half is
// set. So this prints false, and the err != nil check below fires on a nil error.
type MyError struct{}
func (e *MyError) Error() string { return "boom" }
var concreteError *MyError = nil // a nil pointer
var err error = concreteError // ...stored in an interface
fmt.Println(err == nil) // FALSE — the interface holds (*MyError, nil)
if err != nil {
fmt.Println("...and so this branch runs, on a nil error")
} The first half is unremarkable —
any is Any, and a type switch recovers the concrete type. The second half is the most notorious gotcha in the language, and it is worth reading twice: an interface value is a pair of (type, value), so an interface holding a nil pointer is itself not nil. Return a *MyError that happens to be nil from a function declared to return error, and every if err != nil up the call stack fires on a non-existent error. Swift's optionals make this incoherent by construction. The Go rule that avoids it: never declare a function as returning a concrete error pointer type — always return the error interface, and return a literal nil.The Missing Enums
There are no enums
Swift's enums with associated values are one of its best features. Go has nothing comparable, and this is the biggest single expressiveness gap on the page.
// A real sum type: exhaustive, with payloads.
enum Shape {
case circle(radius: Double)
case rectangle(width: Double, height: Double)
}
func area(_ shape: Shape) -> Double {
switch shape { // exhaustive: a missing case will not compile
case .circle(let radius):
return 3.14159 * radius * radius
case .rectangle(let width, let height):
return width * height
}
}
print(area(.circle(radius: 1)))
print(area(.rectangle(width: 2, height: 3)))
// A simple enum, with a raw value.
enum Status: String {
case active, paused
}
print(Status.active.rawValue) // A "constant enum" is iota: typed integer constants, and nothing more.
type Status int
const (
Active Status = iota // 0
Paused // 1
)
// You write String() yourself, or generate it with 'stringer'.
func (status Status) String() string {
if status == Active {
return "active"
}
return "paused"
}
fmt.Println(Active)
// NOTHING stops this. There is no exhaustiveness, and no valid-value check:
var nonsense Status = 42
fmt.Println(int(nonsense))
// A sum type WITH payloads becomes an interface plus one struct per case —
// and the switch is not exhaustive, so 'default' is doing real work.
type Shape interface{ isShape() }
type Circle struct{ Radius float64 }
type Rectangle struct{ Width, Height float64 }
func (Circle) isShape() {} // an unexported marker method: only OUR types
func (Rectangle) isShape() {} // can satisfy Shape
func area(shape Shape) float64 {
switch concrete := shape.(type) {
case Circle:
return 3.14159 * concrete.Radius * concrete.Radius
case Rectangle:
return concrete.Width * concrete.Height
default:
return 0 // add a third Shape and you land HERE, silently, at run time
}
}
fmt.Println(area(Circle{Radius: 1}))
fmt.Println(area(Rectangle{Width: 2, Height: 3})) Two things you rely on are gone.
iota gives you named integer constants, but the type is just an int: nothing prevents Status(42), and there is no rawValue or CaseIterable, so String() is hand-written (or code-generated with stringer). And an enum with payloads has no encoding at all — the community pattern is an interface with an unexported marker method (so only your package's types can satisfy it) plus one struct per case, which gets you the closed set but not exhaustiveness: adding a third Shape compiles fine and quietly falls into default at run time. Coming from Swift, where the compiler catches exactly that, this is the change that will bother you longest.Slices, Maps & Loops
Arrays become slices — and slices alias
A Swift
Array is a value type with copy-on-write, so a copy is a copy. A Go slice is a view into a backing array, so a copy shares storage — and that is a real bug source.var first = [1, 2, 3]
var second = first // a COPY (copy-on-write)
second[0] = 99
print(first) // [1, 2, 3] — untouched
print(second) // [99, 2, 3]
// Slicing also copies.
let slice = first[0..<2]
print(Array(slice))
first.append(4)
print(first, first.count) first := []int{1, 2, 3}
second := first // NOT a copy: both headers point at the SAME backing array
second[0] = 99
fmt.Println(first) // [99 2 3] — mutated through the other name!
fmt.Println(second) // [99 2 3]
// To actually copy, say so.
third := make([]int, len(first))
copy(third, first)
third[0] = 1
fmt.Println(first, third)
// Slicing is also a VIEW, sharing the same array — and append may or may not
// reallocate, depending on capacity, which makes the aliasing intermittent.
view := first[0:2]
view[0] = 7
fmt.Println(first, view, len(view), cap(view))
first = append(first, 4)
fmt.Println(first, len(first), cap(first)) This is the Go trap most likely to bite a Swift developer, because the syntax looks like an array and behaves like a reference. A slice is a small header — pointer, length, capacity — so assigning one copies the header, and both names then write into the same backing array. Worse,
append mutates in place when there is spare capacity and reallocates when there is not, so whether a mutation is visible through another slice depends on capacity, which makes the aliasing intermittent. The rules that keep you safe: copy(destination, source) when you mean a copy, and never assume append left the original alone.Maps, and the loop that replaces map/filter
var ages = ["Ada": 36, "Alan": 41]
ages["Grace"] = 45
print(ages["Ada"] ?? 0)
print(ages["Nobody"] ?? 0) // subscript gives an Optional
// The functional toolkit is built in.
let names = ["Ada", "Grace", "Alan"]
print(names.filter { $0.count > 3 }.map { $0.uppercased() })
print(names.map(\.count).reduce(0, +))
print(names.sorted()) ages := map[string]int{"Ada": 36, "Alan": 41}
ages["Grace"] = 45
fmt.Println(ages["Ada"])
fmt.Println(ages["Nobody"]) // 0 — the ZERO value, not an error and not nil
// "comma ok" is how you tell "missing" from "present but zero".
if age, ok := ages["Nobody"]; !ok {
fmt.Println("no such person", age)
}
// There is no map/filter/reduce in the standard library for slices. You write
// the loop, every time, and Go's position is that the loop is clearer.
names := []string{"Ada", "Grace", "Alan"}
var shouted []string
for _, name := range names {
if len(name) > 3 {
shouted = append(shouted, strings.ToUpper(name))
}
}
fmt.Println(shouted)
total := 0
for _, name := range names {
total += len(name)
}
fmt.Println(total)
sorted := append([]string(nil), names...)
sort.Strings(sorted)
fmt.Println(sorted)
// Map iteration order is RANDOMIZED, deliberately — sort the keys to print.
keys := make([]string, 0, len(ages))
for key := range ages {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
fmt.Println(key, ages[key])
} Two adjustments. A missing map key returns the zero value rather than an optional, so
ages["Nobody"] is 0 and you cannot tell it from someone genuinely aged zero — the comma-ok form (age, ok := ages[key]) is how you ask. And the functional toolkit is not there: no map, no filter, no reduce for slices in the standard library, so you write the for … range loop by hand. (Generics finally made a library version possible, and slices/maps in the standard library are slowly filling in, but the loop remains idiomatic.) Note also that map iteration order is deliberately randomized, so anything printed must be sorted first.Generics
Generics, arrived late and kept small
// Constraints via protocols, with associated types and where clauses.
func largest<T: Comparable>(_ items: [T]) -> T? {
items.max()
}
func mapped<T, U>(_ items: [T], _ transform: (T) -> U) -> [U] {
items.map(transform)
}
print(largest([3, 1, 4]) ?? 0)
print(largest(["b", "a"]) ?? "")
print(mapped([1, 2, 3]) { $0 * 2 })
// Protocols with associated types make a whole family of generic abstractions
// possible — Collection, Sequence, and your own.
struct Stack<Element> {
private var items: [Element] = []
mutating func push(_ item: Element) { items.append(item) }
mutating func pop() -> Element? { items.popLast() }
}
var stack = Stack<Int>()
stack.push(1)
stack.push(2)
print(stack.pop() ?? -1) // Type parameters in square brackets; the constraint is an interface.
// cmp.Ordered is the built-in "things that support <" constraint.
func largest[T int | float64 | string](items []T) (T, bool) {
var zero T
if len(items) == 0 {
return zero, false
}
biggest := items[0]
for _, item := range items[1:] {
if item > biggest {
biggest = item
}
}
return biggest, true
}
// Two type parameters, and the function value as an ordinary argument.
func mapped[T any, U any](items []T, transform func(T) U) []U {
result := make([]U, 0, len(items))
for _, item := range items {
result = append(result, transform(item))
}
return result
}
biggest, _ := largest([]int{3, 1, 4})
fmt.Println(biggest)
word, _ := largest([]string{"b", "a"})
fmt.Println(word)
fmt.Println(mapped([]int{1, 2, 3}, func(number int) int { return number * 2 }))
// Generic types work too...
type Stack[Element any] struct {
items []Element
}
func (stack *Stack[Element]) Push(item Element) {
stack.items = append(stack.items, item)
}
func (stack *Stack[Element]) Pop() (Element, bool) {
var zero Element
if len(stack.items) == 0 {
return zero, false
}
item := stack.items[len(stack.items)-1]
stack.items = stack.items[:len(stack.items)-1]
return item, true
}
// ...but a METHOD cannot introduce its own type parameter. There is no generic
// method, no associated types, and no way to express Swift's Collection protocol.
stack := &Stack[int]{}
stack.Push(1)
stack.Push(2)
top, _ := stack.Pop()
fmt.Println(top) Generics landed in Go 1.18, thirteen years in, and they were kept deliberately small. The basics port fine —
[T any] is <T>, and a constraint is an interface (which may list concrete types, as above, or use cmp.Ordered). What is missing is everything Swift builds its standard library from: no associated types, so there is no way to write the Collection or Sequence protocol; no generic methods, so a type parameter can only be introduced on a function or a type, never on a method of an existing type; and no conditional conformance, no operator constraints, no variadic generics. The result is that Go generics are good for containers and utility functions and are not a foundation for abstraction the way Swift's are — which is, again, on purpose.Strings, Bytes & Runes
A string is bytes; a rune is a code point
let greeting = "Héllo"
// Swift counts what a human calls characters — grapheme clusters.
print(greeting.count) // 5
print(greeting.uppercased())
print(greeting.contains("H"))
for character in greeting {
print(character, terminator: " ")
}
print()
// A Character can be several Unicode scalars, and Swift still counts it as one.
let flag = "🇳🇴"
print(flag.count) // 1
print(flag.unicodeScalars.count) // 2 greeting := "Héllo"
// len() is BYTES. This prints 6, not 5 — é is two bytes in UTF-8.
fmt.Println(len(greeting))
fmt.Println(utf8.RuneCountInString(greeting)) // 5 — code points
fmt.Println(strings.ToUpper(greeting))
fmt.Println(strings.Contains(greeting, "H"))
// Indexing gives a BYTE. Ranging gives runes (code points) and their byte offsets.
fmt.Println(greeting[0]) // 72 — a byte, not a character
for index, character := range greeting {
fmt.Printf("%d:%c ", index, character) // note the index JUMPS over é
}
fmt.Println()
// A rune is a Unicode code point (an int32), NOT a grapheme cluster. A flag is
// two runes, and Go has no built-in notion of the thing a human calls a character.
flag := "🇳🇴"
fmt.Println(len(flag)) // 8 bytes
fmt.Println(utf8.RuneCountInString(flag)) // 2 runes
fmt.Println([]rune(flag)) Go's string is a read-only slice of bytes that happens to hold UTF-8, and the language is explicit about all three levels:
len() is bytes, utf8.RuneCountInString is code points, and range over a string yields runes with their byte offsets (which is why the index jumps). What Go does not have is Swift's grapheme cluster — a rune is a code point, so an emoji flag is two of them, and "what a human calls one character" needs a third-party package. Swift's String is the most Unicode-correct of any mainstream language and it charges you for that with an Index type you cannot do integer arithmetic on; Go charges you nothing and gives you bytes. Neither is wrong, but you should know which one you are holding.Goroutines & Channels
Task becomes go — and nothing awaits it
The reason to learn Go, and a genuinely different model from Swift's. A goroutine is a cheap thread over shared memory; there is no
await, no future, and no value handed back.import Foundation
func work(_ id: Int) async -> String {
try? await Task.sleep(for: .milliseconds(10))
return "worker \(id)"
}
// Structured concurrency: the group owns its children, and awaiting it collects
// the results. The task tree also propagates cancellation for you.
func runAll() async -> [String] {
await withTaskGroup(of: String.self) { group in
for id in 1...3 {
group.addTask { await work(id) }
}
var results: [String] = []
for await result in group {
results.append(result)
}
return results.sorted()
}
}
print(await runAll()) func work(id int) string {
time.Sleep(10 * time.Millisecond)
return fmt.Sprintf("worker %d", id)
}
// 'go' starts a goroutine. It returns NOTHING — not a handle, not a future —
// so getting a result back means sending it somewhere.
results := make(chan string, 3)
var waitGroup sync.WaitGroup
for id := 1; id <= 3; id++ {
waitGroup.Add(1)
go func() { // since 1.22, the loop variable is per-iteration: no capture bug
defer waitGroup.Done()
results <- work(id)
}()
}
// A WaitGroup is the join. There is no parent that owns these goroutines: if
// you forget to wait, main exits and they are killed mid-flight.
waitGroup.Wait()
close(results)
var collected []string
for result := range results {
collected = append(collected, result)
}
sort.Strings(collected)
fmt.Println(collected) Three assumptions to drop. A goroutine returns nothing —
go f() is fire-and-forget, so a result comes back over a channel, and there is no Task handle to await. Nothing owns it: there is no task tree, no structured concurrency, and no automatic cancellation, so a goroutine that outlives its purpose is a leak (Go's single most common production bug), and one still running when main returns is simply killed. And the coordination primitive is the sync.WaitGroup, which is a counter you must remember to Add to and Done. What you get for all that is a model where blocking is free — a goroutine costs about 2 KB, blocking one costs nothing, and there is no function coloring at all: no async, no await, and no separate world of asynchronous functions to keep in your head.Channels and select
import Foundation
// Swift's nearest equivalent is an AsyncStream: a sequence you await.
func ticks() -> AsyncStream<Int> {
AsyncStream { continuation in
Task {
for number in 1...3 {
try? await Task.sleep(for: .milliseconds(10))
continuation.yield(number)
}
continuation.finish()
}
}
}
for await value in ticks() {
print("got \(value)")
}
// There is no 'select' over several streams: racing two of them means
// TaskGroup, or a hand-rolled merge.
print("done") // A channel is a typed pipe. Sending blocks until someone receives (unbuffered),
// which makes it a synchronization primitive, not just a queue.
ticks := make(chan int)
go func() {
for number := 1; number <= 3; number++ {
time.Sleep(10 * time.Millisecond)
ticks <- number
}
close(ticks) // closing tells the receiver the stream is over
}()
// Ranging over a channel receives until it is closed.
for value := range ticks {
fmt.Println("got", value)
}
// select is the feature with no Swift equivalent: it waits on SEVERAL channels
// at once and takes whichever is ready first. This is how you build timeouts,
// cancellation, and fan-in — all with one primitive.
slow := make(chan string)
go func() {
time.Sleep(50 * time.Millisecond)
slow <- "finally"
}()
select {
case message := <-slow:
fmt.Println(message)
case <-time.After(10 * time.Millisecond):
fmt.Println("timed out")
}
fmt.Println("done") A channel is a typed pipe, and an unbuffered one is a rendezvous: the send blocks until a receiver takes it, which makes it a synchronization primitive rather than just a queue. Swift's nearest thing is
AsyncStream, and it covers the streaming case well. What Swift has no answer to is select — waiting on several channels at once and proceeding with whichever is ready first. That single primitive is how Go expresses timeouts (time.After), cancellation (a context's Done() channel), fan-in, and back-pressure, all with the same syntax. It is the piece of the language that most rewards learning, and the reason "share memory by communicating" is a slogan rather than a platitude.No actors: a mutex, and a race detector
// An actor serializes access to its own state. The COMPILER guarantees no two
// tasks touch it at once — a data race on 'count' is not expressible.
actor Counter {
private var count = 0
func increment() { count += 1 }
func value() -> Int { count }
}
let counter = Counter()
await withTaskGroup(of: Void.self) { group in
for _ in 1...4 {
group.addTask { await counter.increment() }
}
}
print(await counter.value())
// Swift 6 goes further: Sendable is checked at compile time, so passing
// non-thread-safe state across a concurrency boundary is a compile ERROR. // No actors. Shared mutable state is protected by a lock you take by hand, and
// nothing in the type system knows the lock is related to the data.
type Counter struct {
mutex sync.Mutex
count int
}
func (counter *Counter) Increment() {
counter.mutex.Lock()
defer counter.mutex.Unlock() // defer is what makes this bearable
counter.count++
}
func (counter *Counter) Value() int {
counter.mutex.Lock()
defer counter.mutex.Unlock()
return counter.count
}
counter := &Counter{}
var waitGroup sync.WaitGroup
for i := 0; i < 4; i++ {
waitGroup.Add(1)
go func() {
defer waitGroup.Done()
counter.Increment()
}()
}
waitGroup.Wait()
fmt.Println(counter.Value())
// Forget the Lock() and this compiles, ships, and is wrong intermittently. Go's
// answer is a RUNTIME race detector (go test -race), not a compile-time one. Here the two languages take opposite bets. Swift 6 puts concurrency safety in the type system: an actor serializes its own state,
Sendable is checked at compile time, and a data race becomes a compile error. Go puts it in the tooling: you take a sync.Mutex by hand, nothing connects the lock to the data it protects, forgetting it compiles perfectly — and go test -race finds the bug at run time, if your test happens to exercise the interleaving. The Go rejoinder is that channels let you avoid shared state in the first place ("do not communicate by sharing memory; share memory by communicating"), and that a small language with a great race detector beats a large one with a strict compiler. Having just come from Swift 6's strict concurrency checking, you are entitled to your own opinion about that.