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.