PONY λ M2 Modula-2

Go.CodeCompared.To/Ruby

An interactive executable cheatsheet comparing Go and Ruby

Go 1.26.2 Ruby 4.0
Hello World & Running
Hello, World
package main import "fmt" func main() { fmt.Println("Hello, World!") }
puts "Hello, World!"
Ruby has no package clause, no import, and no main function. Top-level code runs top to bottom the moment the file is loaded. puts writes a value followed by a newline — the everyday equivalent of fmt.Println.
Running a program
// Compile and run in one step: // go run main.go // Build a standalone binary: // go build -o hello main.go && ./hello // No runtime needed to ship the binary.
# Run directly — no build step, no binary: # ruby hello.rb # # One-liner without a file: # ruby -e 'puts "Hi"' # # Interactive REPL: # irb
Ruby is interpreted: there is no go build, no compiled binary, and no separate toolchain to invoke. ruby hello.rb parses and runs in one shot, and irb gives you a REPL for quick experiments. The trade-off is that the Ruby interpreter must be present wherever the script runs.
Comments
package main import "fmt" func main() { // Single-line comment /* Block comment */ count := 0 // inline fmt.Println(count) }
# Single-line comment =begin Block comment spanning several lines =end count = 0 # inline puts count
Ruby uses # for single-line comments instead of //. For block comments it uses =begin/=end, which must start in column one — there is no /* ... */ form.
Imports & requiring code
package main import ( "fmt" "strings" ) func main() { fmt.Println(strings.ToUpper("ruby")) }
require "json" person = { name: "Alice", age: 30 } puts JSON.generate(person)
Where Go imports packages by path and references them through a qualifier (strings.ToUpper), Ruby requires a library by name and its constants and methods become available globally (JSON.generate). The standard library ships with Ruby, so no module download or go.mod entry is needed.
Variables & Types
Declaring variables
package main import "fmt" func main() { var name string = "Ruby" age := 31 // short declaration, type inferred var ready bool = true fmt.Println(name, age, ready) }
name = "Ruby" age = 31 ready = true puts "#{name} #{age} #{ready}"
Ruby has no var, no :=, and no type annotations — a variable springs into existence on first assignment and can later hold a value of any type. There is also no "declared but not used" error, so an unused variable is perfectly legal.
Zero values vs nil
package main import "fmt" func main() { var count int // 0 var label string // "" var active bool // false var items []int // nil slice fmt.Printf("%d %q %t %v\n", count, label, active, items) }
count = nil label = nil active = nil items = nil puts "#{count.inspect} #{label.inspect} #{active.inspect} #{items.inspect}"
Go gives every declared variable a typed zero value (0, "", false, nil). Ruby has a single nil for "nothing" and no concept of an uninitialised-but-typed variable — referencing an unassigned name raises NameError rather than yielding a zero value.
Constants
package main import "fmt" const Pi = 3.14159 const Greeting = "hello" func main() { fmt.Println(Pi, Greeting) }
PI = 3.14159 GREETING = "hello" puts PI puts GREETING
In Ruby any identifier that begins with a capital letter is a constant, so the convention is SCREAMING_SNAKE_CASE. Unlike Go, Ruby constants are not truly immutable — reassigning one only prints a warning rather than failing to compile — so they are a convention enforced socially, not by the language.
Type conversion
package main import "fmt" func main() { count := 5 ratio := 2.5 total := float64(count) * ratio fmt.Println(total) fmt.Println("count is " + fmt.Sprint(count)) }
count = 5 ratio = 2.5 total = count * ratio puts total puts "count is " + count.to_s
Go forbids mixing numeric types and requires an explicit float64(count) conversion. Ruby coerces an Integer to a Float automatically in arithmetic. Concatenating a number into a string still needs an explicit .to_s, however — Ruby will not implicitly stringify the way it implicitly widens numbers.
Strings
String basics
package main import "fmt" func main() { language := "Ruby" fmt.Println(len(language)) fmt.Println(language[0:2]) fmt.Println(language + " rocks") }
language = "Ruby" puts language.length puts language[0, 2] puts language + " rocks"
Ruby strings are full objects with hundreds of methods. length counts characters (not bytes), and slicing uses str[start, count] or a range. Note that string literals are frozen by default in Ruby 4.0, so to build a string in place you start from +"..." (a mutable copy) and append with <<.
Interpolation vs formatting
package main import "fmt" func main() { name := "World" count := 3 fmt.Printf("Hello, %s!\n", name) message := fmt.Sprintf("Repeated %d times", count) fmt.Println(message) }
name = "World" count = 3 puts "Hello, #{name}!" message = "Repeated #{count} times" puts message
Ruby builds strings with inline interpolation: #{expression} inside a double-quoted string evaluates any Ruby code and inserts the result. There is no need for Printf verbs like %s or %d — though Ruby does offer format/sprintf with the same C-style verbs when you want explicit formatting.
Common string methods
package main import ( "fmt" "strings" ) func main() { sentence := "the quick brown fox" fmt.Println(strings.ToUpper(sentence)) fmt.Println(strings.Split(sentence, " ")) fmt.Println(strings.Contains(sentence, "quick")) fmt.Println(strings.Replace(sentence, "quick", "slow", 1)) }
sentence = "the quick brown fox" puts sentence.upcase p sentence.split(" ") puts sentence.include?("quick") puts sentence.sub("quick", "slow")
Ruby calls these as methods on the string itself rather than passing the string to a strings package function. A method ending in ? conventionally returns a boolean (include?), and sub replaces the first match while gsub replaces them all. p prints a value’s inspect form, handy for seeing array structure.
Characters vs bytes
package main import ( "fmt" "unicode/utf8" ) func main() { word := "héllo" fmt.Println(len(word)) // bytes fmt.Println(utf8.RuneCountInString(word)) // characters for index, runeValue := range word { fmt.Printf("%d:%c ", index, runeValue) } fmt.Println() }
word = "héllo" puts word.bytesize # bytes puts word.length # characters word.each_char.with_index do |character, index| print "#{index}:#{character} " end puts
In Go a string is a byte sequence and indexing yields bytes, so multibyte UTF-8 needs range or the utf8 package to walk runes. Ruby strings are character-aware by default: length already counts characters and each_char iterates them, with bytesize available when you specifically want bytes.
Numbers
Integer and float division
package main import "fmt" func main() { fmt.Println(7 / 2) // 3 (integer division) fmt.Println(7.0 / 2.0) // 3.5 fmt.Println(float64(7) / 2) // 3.5 fmt.Println(7 % 2) // 1 }
puts 7 / 2 # 3 (integer division) puts 7.0 / 2.0 # 3.5 puts 7.fdiv(2) # 3.5 puts 7 % 2 # 1
Both languages do integer division when both operands are integers. Where Go makes you convert one side with float64(...), Ruby offers Integer#fdiv for an explicit float result, or you can simply write one literal as 7.0.
Arbitrary-precision integers
package main import ( "fmt" "math/big" ) func main() { result := new(big.Int).Exp( big.NewInt(2), big.NewInt(100), nil) fmt.Println(result) }
result = 2 ** 100 puts result puts result.class
A Go int is a fixed 64-bit value that silently overflows, so large integers require the math/big package and its method-based arithmetic. Ruby has no such ceiling: an Integer transparently promotes to arbitrary precision, so 2 ** 100 just works and stays an ordinary Integer.
Formatting numbers
package main import "fmt" func main() { price := 1234.5 fmt.Printf("%.2f\n", price) fmt.Printf("%08.2f\n", price) fmt.Printf("%x\n", 255) }
price = 1234.5 puts format("%.2f", price) puts format("%08.2f", price) puts format("%x", 255)
Ruby’s format (aliased as sprintf) uses the same C-style format verbs you already know from fmt.Printf. The difference is that it returns the formatted string rather than printing it, so you pair it with puts — or use the terse "%.2f" % price operator form.
Arrays & Slices
Slices vs arrays
package main import "fmt" func main() { numbers := []int{1, 2, 3} numbers = append(numbers, 4, 5) fmt.Println(numbers) fmt.Println(numbers[1:3]) fmt.Println(len(numbers)) }
numbers = [1, 2, 3] numbers.push(4, 5) p numbers p numbers[1..2] puts numbers.length
Ruby’s Array is one growable, heterogeneous type — there is no separate fixed array vs slice distinction, no append reassignment dance, and no capacity to track. push (or <<) mutates in place, and a Range like 1..2 slices it. An array may freely hold mixed types.
Iterating
package main import "fmt" func main() { fruits := []string{"apple", "banana", "cherry"} for index, fruit := range fruits { fmt.Printf("%d: %s\n", index, fruit) } }
fruits = ["apple", "banana", "cherry"] fruits.each_with_index do |fruit, index| puts "#{index}: #{fruit}" end
Instead of a for ... range loop, Ruby asks the collection to iterate itself by handing a block to each_with_index. The block parameters appear between |...|. Note the order is reversed from Go’s index, value: Ruby yields value, index.
Map, filter, reduce
package main import "fmt" func main() { numbers := []int{1, 2, 3, 4, 5} doubled := []int{} for _, number := range numbers { doubled = append(doubled, number*2) } sum := 0 for _, number := range numbers { if number%2 == 0 { sum += number } } fmt.Println(doubled, sum) }
numbers = [1, 2, 3, 4, 5] doubled = numbers.map { |number| number * 2 } sum = numbers.select(&:even?).sum p doubled puts sum
This is where Ruby diverges most sharply from Go. The standard library has no map/filter/reduce, so Go expresses every transformation as an explicit loop. Ruby’s Enumerable module gives every collection map, select, reject, reduce, sum, and dozens more, chained together as a pipeline.
Sorting
package main import ( "fmt" "sort" ) func main() { numbers := []int{3, 1, 4, 1, 5, 9, 2} sort.Ints(numbers) fmt.Println(numbers) words := []string{"banana", "apple", "cherry"} sort.Slice(words, func(left, right int) bool { return len(words[left]) < len(words[right]) }) fmt.Println(words) }
numbers = [3, 1, 4, 1, 5, 9, 2] p numbers.sort words = ["banana", "apple", "cherry"] p words.sort_by(&:length)
Array#sort returns a new sorted array, leaving the original untouched (use sort! to sort in place). For a custom key, sort_by takes a block that maps each element to its sort key — much terser than Go’s sort.Slice comparator, and it computes each key only once.
Maps & Hashes
Maps vs hashes
package main import "fmt" func main() { ages := map[string]int{"Alice": 30, "Bob": 25} ages["Carol"] = 35 delete(ages, "Bob") fmt.Println(ages["Alice"], len(ages)) }
ages = { "Alice" => 30, "Bob" => 25 } ages["Carol"] = 35 ages.delete("Bob") puts "#{ages["Alice"]} #{ages.length}"
Ruby’s Hash is the counterpart to a Go map. A key difference: a Ruby Hash preserves insertion order when you iterate, whereas Go deliberately randomises map iteration order. Keys may be any object, not just one declared key type.
Missing keys
package main import "fmt" func main() { inventory := map[string]int{"apples": 5} count, ok := inventory["bananas"] if !ok { fmt.Println("not stocked") } fmt.Println(count) // 0, the zero value }
inventory = { "apples" => 5 } count = inventory["bananas"] puts "not stocked" if count.nil? puts inventory.fetch("bananas", 0) # default
Go’s comma-ok idiom distinguishes "absent" from "present but zero". Ruby returns nil for a missing key by default (so you check with nil?), and fetch lets you supply a fallback or raise KeyError when a key truly must exist.
Symbol keys
package main import "fmt" func main() { // Go has no symbol type; string keys are idiomatic. person := map[string]string{ "name": "Alice", "role": "admin", } fmt.Println(person["name"], person["role"]) }
person = { name: "Alice", role: "admin" } puts person[:name] puts person[:role] puts person.keys.inspect
Ruby has a Symbol type — an immutable, interned identifier written :name — that Go lacks entirely. Symbols are the idiomatic key for hashes that act like records, because they are faster to compare and never duplicated in memory. The name: value syntax is shorthand for :name => value.
Control Flow
Conditionals
package main import "fmt" func main() { score := 85 if score >= 90 { fmt.Println("A") } else if score >= 80 { fmt.Println("B") } else { fmt.Println("C") } }
score = 85 if score >= 90 puts "A" elsif score >= 80 puts "B" else puts "C" end
Ruby uses elsif (not else if), drops the parentheses around the condition, and closes the block with end instead of braces. Ruby also has a trailing-modifier form — puts "B" if score >= 80 — and an unless keyword for negated conditions.
Loops
package main import "fmt" func main() { for index := 0; index < 3; index++ { fmt.Println(index) } count := 0 for count < 3 { count++ } fmt.Println("count:", count) }
3.times do |index| puts index end count = 0 while count < 3 count += 1 end puts "count: #{count}"
Go folds every loop into the single for keyword. Ruby keeps while (and until) but rarely uses a C-style counter loop — idiomatic Ruby asks an object to iterate: 3.times, (1..5).each, or array.each. The integer literal 3 responds to times because everything, including numbers, is an object.
Switch vs case
package main import "fmt" func main() { day := "Sat" switch day { case "Sat", "Sun": fmt.Println("weekend") default: fmt.Println("weekday") } }
day = "Sat" result = case day when "Sat", "Sun" then "weekend" else "weekday" end puts result
Both stop after the first match without fall-through. Two differences stand out: Ruby’s case is an expression that returns a value, so it can be assigned directly; and when compares with the === operator, which means you can match against ranges, classes, and regular expressions, not just equal values.
Everything is an expression
package main import "fmt" func main() { score := 75 // Go has no ternary; use an if statement. grade := "fail" if score >= 60 { grade = "pass" } fmt.Println(grade) }
score = 75 grade = if score >= 60 then "pass" else "fail" end puts grade # Ternary also exists: status = score >= 60 ? "ok" : "low" puts status
Go statements do not produce values and the language intentionally omits the ternary operator. In Ruby almost everything is an expression: an if/else evaluates to its last expression, so you can assign its result directly. The C-style condition ? a : b ternary is also available.
Functions & Methods
Defining functions
package main import "fmt" func greet(name string) string { return "Hello, " + name } func main() { fmt.Println(greet("Ruby")) }
def greet(name) "Hello, #{name}" end puts greet("Ruby")
A Ruby method needs no parameter types, no return type, and usually no return keyword — the value of the last evaluated expression is returned automatically. Ruby 4.0 also supports an endless one-liner form: def greet(name) = "Hello, #{name}".
Multiple return values
package main import "fmt" func minMax(numbers []int) (int, int) { smallest, largest := numbers[0], numbers[0] for _, number := range numbers { if number < smallest { smallest = number } if number > largest { largest = number } } return smallest, largest } func main() { low, high := minMax([]int{3, 1, 4, 1, 5}) fmt.Println(low, high) }
def min_max(numbers) [numbers.min, numbers.max] end low, high = min_max([3, 1, 4, 1, 5]) puts "#{low} #{high}"
Go has true multiple return values; Ruby fakes them by returning an Array and relying on destructuring assignment to unpack it (low, high = ...). The same mechanism splats into the left-hand side, so first, *rest = [1, 2, 3] binds rest to [2, 3].
Variadic parameters
package main import "fmt" func sum(numbers ...int) int { total := 0 for _, number := range numbers { total += number } return total } func main() { fmt.Println(sum(1, 2, 3, 4)) }
def sum(*numbers) numbers.sum end puts sum(1, 2, 3, 4)
Ruby’s splat operator *numbers gathers any number of positional arguments into an array, the direct analogue of Go’s ...int. To spread an existing array back into arguments, prefix it with * at the call site: sum(*[1, 2, 3]).
Default & keyword arguments
package main import "fmt" // Go has neither default nor keyword arguments; // callers must pass every value positionally. func connect(host string, port int, secure bool) string { return fmt.Sprintf("%s:%d secure=%t", host, port, secure) } func main() { fmt.Println(connect("localhost", 5432, true)) }
def connect(host:, port: 5432, secure: false) "#{host}:#{port} secure=#{secure}" end puts connect(host: "localhost", secure: true)
Ruby supports both default values and named keyword arguments, which Go omits entirely. A keyword written port: 5432 is optional with a default, while host: with no default is required. Callers pass them by name in any order, which removes the long positional argument lists Go forces.
Closures & Blocks
Closures
package main import "fmt" func makeCounter() func() int { count := 0 return func() int { count++ return count } } func main() { counter := makeCounter() fmt.Println(counter(), counter(), counter()) }
def make_counter count = 0 -> { count += 1 } end counter = make_counter puts "#{counter.call} #{counter.call} #{counter.call}"
Both languages have closures that capture surrounding variables by reference. Ruby’s -> { ... } creates a lambda (a Proc object); you invoke it with .call, the bracket form counter[], or counter.(). The captured count lives on for as long as the lambda does.
Blocks & yield
package main import "fmt" // The closest Go idiom: pass a function value. func repeat(times int, action func(int)) { for index := 0; index < times; index++ { action(index) } } func main() { repeat(3, func(index int) { fmt.Println("tick", index) }) }
def repeat(times) index = 0 while index < times yield index index += 1 end end repeat(3) do |index| puts "tick #{index}" end
A block is Ruby’s signature feature and has no Go equivalent. Every method may be passed one anonymous block of code, which it runs with yield. This is why iteration in Ruby reads as collection.each do |item| ... end rather than a language-level loop — the loop is just a method receiving a block.
Procs vs lambdas
package main import "fmt" func main() { // A reusable function value: double := func(number int) int { return number * 2 } numbers := []int{1, 2, 3} for _, number := range numbers { fmt.Print(double(number), " ") } fmt.Println() }
double = ->(number) { number * 2 } numbers = [1, 2, 3] p numbers.map(&double) # Turn a method into a block argument: p numbers.map(&:to_s)
A stored function in Ruby is a Proc or lambda. The & operator converts one into the block a method expects, so map(&double) applies it to each element. The symbol form &:to_s is shorthand for "call this method on each item" — an idiom you will see everywhere in Ruby.
Structs & Classes
Structs vs classes
package main import "fmt" type Point struct { X, Y int } func main() { point := Point{X: 1, Y: 2} fmt.Println(point.X, point.Y) fmt.Printf("%+v\n", point) }
class Point attr_accessor :x, :y def initialize(x, y) @x = x @y = y end end point = Point.new(1, 2) puts "#{point.x} #{point.y}"
Ruby has classes rather than plain structs. State lives in instance variables prefixed with @, which are private by default; attr_accessor generates the getter and setter methods that expose them. The constructor is always named initialize and is invoked by Point.new.
Methods
package main import "fmt" type Rectangle struct { Width, Height int } func (rectangle Rectangle) Area() int { return rectangle.Width * rectangle.Height } func main() { rectangle := Rectangle{Width: 3, Height: 4} fmt.Println(rectangle.Area()) }
class Rectangle def initialize(width, height) @width = width @height = height end def area @width * @height end end puts Rectangle.new(3, 4).area
Go attaches methods to a type through an explicit receiver written before the method name. Ruby defines methods inside the class body, and they implicitly operate on self (the current instance). Because the receiver is implicit, a method refers to the object’s own state simply by naming the @ variables.
Embedding vs inheritance
package main import "fmt" type Animal struct { Name string } func (animal Animal) Speak() string { return animal.Name + " makes a sound" } type Dog struct { Animal // embedded Breed string } func main() { dog := Dog{Animal: Animal{Name: "Rex"}, Breed: "Lab"} fmt.Println(dog.Speak()) }
class Animal def initialize(name) @name = name end def speak "#{@name} makes a sound" end end class Dog < Animal def speak super + " (woof)" end end puts Dog.new("Rex").speak
Go composes behaviour by embedding one struct in another and promoting its methods. Ruby has classical single inheritance with class Dog < Animal, and a subclass can override a method and still reach the parent’s version with super — there is genuine "is-a" inheritance, which Go deliberately avoids.
Open classes
package main import "fmt" // Go cannot add methods to a type it does not own, // so a free function is the usual workaround. func shout(text string) string { return text + "!" } func main() { fmt.Println(shout("hello")) }
class String def shout upcase + "!" end end puts "hello".shout
Ruby classes are open: you can reopen any class — even a built-in like String or Integer — and add or redefine methods at runtime. Go forbids declaring a method on a type from another package, which is why utilities there are free functions. Ruby’s power here comes with the responsibility not to surprise other code.
Interfaces & Duck Typing
Interfaces vs duck typing
package main import "fmt" type Speaker interface { Speak() string } type Cat struct{} func (cat Cat) Speak() string { return "meow" } func announce(speaker Speaker) { fmt.Println(speaker.Speak()) } func main() { announce(Cat{}) }
class Cat def speak "meow" end end def announce(speaker) puts speaker.speak end announce(Cat.new)
Go has structural interfaces: a type satisfies Speaker automatically by having a Speak method. Ruby goes further and drops the interface declaration altogether — announce simply calls speak, and any object that responds to it works. This is duck typing: "if it quacks like a duck, it is a duck."
Modules as mixins
package main import ( "fmt" "sort" ) type Person struct { Name string Age int } func main() { people := []Person{{"Alice", 30}, {"Bob", 25}} sort.Slice(people, func(left, right int) bool { return people[left].Age < people[right].Age }) fmt.Println(people) }
class Person include Comparable attr_reader :name, :age def initialize(name, age) @name = name @age = age end def <=>(other) age <=> other.age end end people = [Person.new("Alice", 30), Person.new("Bob", 25)] puts people.sort.map(&:name).inspect
Ruby mixes shared behaviour in with modules. By include Comparable and defining a single <=> ("spaceship") method, a class gains <, >, sort, min, max, and between? for free. This is closer to Go interfaces granting capabilities, but the module supplies real method implementations, not just a contract.
Type switch vs respond_to?
package main import "fmt" func describe(value any) string { switch typed := value.(type) { case int: return fmt.Sprintf("int: %d", typed) case string: return fmt.Sprintf("string: %q", typed) default: return "unknown" } } func main() { fmt.Println(describe(42)) fmt.Println(describe("hi")) }
def describe(value) case value when Integer then "int: #{value}" when String then "string: #{value.inspect}" else "unknown" end end puts describe(42) puts describe("hi")
Go’s type switch inspects the dynamic type held by an any. Ruby’s case does the same because when Integer uses Integer === value, which is true when value is an instance. Idiomatic Ruby often prefers value.respond_to?(:each) — asking whether an object can do something rather than what class it is.
Error Handling
Error values vs exceptions
package main import ( "errors" "fmt" ) func divide(numerator, denominator int) (int, error) { if denominator == 0 { return 0, errors.New("division by zero") } return numerator / denominator, nil } func main() { result, err := divide(10, 0) if err != nil { fmt.Println("error:", err) return } fmt.Println(result) }
def divide(numerator, denominator) raise ArgumentError, "division by zero" if denominator.zero? numerator / denominator end begin puts divide(10, 0) rescue ArgumentError => error puts "error: #{error.message}" end
This is the deepest philosophical split between the two languages. Go returns errors as ordinary values you must check after every call. Ruby raises exceptions that unwind the stack until a rescue catches them, so the happy path stays uncluttered by if err != nil and errors propagate automatically until something handles them.
Cleanup: defer vs ensure
package main import "fmt" func process() { defer fmt.Println("cleanup runs last") fmt.Println("working") } func main() { process() }
def process puts "working" ensure puts "cleanup runs last" end process
Go’s defer schedules cleanup to run when the function returns. Ruby’s ensure clause plays the same role: its body runs whether the method finishes normally or an exception is raised, making it the place to close files or release locks. Unlike defer, it is attached to a begin/end block rather than stacked per statement.
Custom error types
package main import "fmt" type NotFoundError struct { Key string } func (notFound NotFoundError) Error() string { return "not found: " + notFound.Key } func lookup(key string) error { return NotFoundError{Key: key} } func main() { err := lookup("user:42") fmt.Println(err) }
class NotFoundError < StandardError def initialize(key) super("not found: #{key}") end end begin raise NotFoundError.new("user:42") rescue NotFoundError => error puts error.message end
A Go custom error is any type implementing the Error() string method. A Ruby custom exception is a class that inherits from StandardError; rescue can then match it by class, and subclasses let you build an exception hierarchy that callers rescue at whatever level of specificity they need.
Panic/recover vs raise/rescue
package main import "fmt" func safeDivide(numerator, denominator int) (result int) { defer func() { if recovered := recover(); recovered != nil { fmt.Println("recovered:", recovered) result = -1 } }() return numerator / denominator } func main() { fmt.Println(safeDivide(10, 0)) }
def safe_divide(numerator, denominator) numerator / denominator rescue ZeroDivisionError => error puts "recovered: #{error.message}" -1 end puts safe_divide(10, 0)
In Go, dividing by zero panics, and only a deferred recover can stop the unwinding — panics are reserved for truly exceptional situations. In Ruby that same division raises an ordinary ZeroDivisionError, caught by a normal rescue. Note the method-level rescue: no explicit begin is needed when rescuing the whole method body.
Concurrency
Goroutines vs threads
package main import ( "fmt" "sync" ) func main() { var waitGroup sync.WaitGroup waitGroup.Add(1) go func() { defer waitGroup.Done() fmt.Println("hello from goroutine") }() waitGroup.Wait() fmt.Println("main done") }
thread = Thread.new do puts "hello from thread" end thread.join puts "main done"
Go starts a lightweight goroutine with go and coordinates with a WaitGroup. Ruby’s Thread.new starts an OS-backed thread, and join waits for it to finish. A caution for Go programmers: a global interpreter lock in the standard CRuby means threads do not run pure-Ruby code in parallel — they shine for I/O, not CPU-bound work.
Channels vs queues
package main import "fmt" func main() { messages := make(chan int, 3) go func() { for index := 0; index < 3; index++ { messages <- index * index } close(messages) }() for value := range messages { fmt.Println(value) } }
queue = Queue.new producer = Thread.new do 3.times { |index| queue << index * index } end producer.join results = [] results << queue.pop until queue.empty? p results
A channel is central to Go’s "share by communicating" model. Ruby’s nearest equivalent is Queue (a thread-safe FIFO from the standard library): producers push with << and consumers pop, which blocks until an item is available. There is no close with range semantics, so consumers loop until the queue is empty or read a sentinel value.
Collecting parallel results
package main import ( "fmt" "sync" ) func main() { results := make([]int, 4) var waitGroup sync.WaitGroup for index := 0; index < 4; index++ { waitGroup.Add(1) go func(position int) { defer waitGroup.Done() results[position] = position * position }(index) } waitGroup.Wait() fmt.Println(results) }
threads = (0...4).map do |index| Thread.new(index) { |position| position * position } end results = threads.map(&:value) p results
Ruby’s Thread#value returns the value of the thread’s block, blocking until the thread completes — so threads.map(&:value) gathers every result in order without a shared slice or a WaitGroup. Passing index into Thread.new(index) binds it per iteration, the same care you take when capturing a loop variable in a goroutine.
Mutexes
package main import ( "fmt" "sync" ) func main() { var mutex sync.Mutex counter := 0 var waitGroup sync.WaitGroup for index := 0; index < 5; index++ { waitGroup.Add(1) go func() { defer waitGroup.Done() mutex.Lock() counter++ mutex.Unlock() }() } waitGroup.Wait() fmt.Println(counter) }
mutex = Mutex.new counter = 0 threads = 5.times.map do Thread.new do mutex.synchronize { counter += 1 } end end threads.each(&:join) puts counter
Ruby’s Mutex mirrors sync.Mutex, but its synchronize method takes a block and releases the lock automatically when the block exits — even on an exception — so you never pair a manual Lock/Unlock by hand the way Go (without defer) requires.
Pattern Matching
Array patterns
package main import "fmt" func main() { point := []int{0, 5} // Go has no destructuring match; inspect manually. switch { case point[0] == 0 && point[1] == 0: fmt.Println("origin") case point[0] == 0: fmt.Printf("on y-axis at %d\n", point[1]) default: fmt.Println("elsewhere") } }
point = [0, 5] case point in [0, 0] puts "origin" in [0, y] puts "on y-axis at #{y}" else puts "elsewhere" end
Ruby’s case/in (stable since Ruby 3.0) destructures and matches in one step, and binds variables as it goes — the pattern [0, y] matches a two-element array whose first item is 0 and captures the second as y. Go has no structural pattern matching, so the same logic becomes a series of manual index checks.
Hash patterns
package main import "fmt" func main() { user := map[string]any{"role": "admin", "name": "Alice"} role, _ := user["role"].(string) switch role { case "admin": fmt.Printf("admin: %s\n", user["name"]) default: fmt.Println("guest") } }
user = { role: "admin", name: "Alice" } case user in { role: "admin", name: } puts "admin: #{name}" else puts "guest" end
A hash pattern matches the keys you name and ignores the rest, binding their values to like-named variables — name: with no value captures user[:name] into name. Combined with a literal like role: "admin", it both filters and extracts in a single clause, replacing the type-assert-and-compare dance Go requires over an any map.
Type + structure patterns
package main import "fmt" func classify(value any) string { switch typed := value.(type) { case []int: if len(typed) == 2 { return fmt.Sprintf("pair %d,%d", typed[0], typed[1]) } return "list" case int: return fmt.Sprintf("number %d", typed) default: return "other" } } func main() { fmt.Println(classify([]int{3, 4})) fmt.Println(classify(7)) }
def classify(value) case value in [Integer => first, Integer => second] "pair #{first},#{second}" in Integer => number "number #{number}" else "other" end end puts classify([3, 4]) puts classify(7)
Ruby patterns combine type checks and structure in one clause: [Integer => first, Integer => second] matches a two-element array of integers and binds both at once. This fuses Go’s type switch and length check into a single, declarative branch — one of the clearest places where Ruby’s expressiveness shows against Go’s explicitness.