Go is everything I wanted C to be

,

I’ve used C a little and mostly for scientific computing, but I’ve recently learned about some of the details of the Go programming language. It just…is everything I didn’t know I wanted from C (especially for strings). Now, don’t get me wrong, C has it’s place and I’m not running from Julia to Go for scientific computing.

Because I have to go to work, I’ve used Claude to summarize the key components of Go:

  • Static typing with type inference
  • Compiled to native code with fast startup time
  • Built-in concurrency with goroutines and channels
  • Single binary deployment with no external dependencies
  • Garbage collection with low latency
  • Strong standard library for systems programming
  • Explicit error handling with multiple return values

and build a short “Monte-Carlo Pi calculation” to help translate

Julia

using Statistics

function calculate_pi(samples)
    inside = 0
    for i in 1:samples
        x, y = rand(), rand()
        if x^2 + y^2 <= 1
            inside += 1
        end
    end
    return 4 * inside / samples
end

function main()
    workers = Threads.nthreads()
    samples_per_worker = 1_000_000
    
    # Array to store results from each thread
    results = zeros(workers)
    
    # Use Threads.@threads for loop-level parallelism
    Threads.@threads for i in 1:workers
        results[i] = calculate_pi(samples_per_worker)
    end
    
    pi_estimate = mean(results)
    println("π ≈ $(round(pi_estimate, digits=10))")
end

main()

(Me) Then of course run this with julia script.jl.

Go

package main

import (
	"fmt"
	"math/rand"
	"sync"
)

func calculatePi(samples int) float64 {
	r := rand.New(rand.NewSource(int64(samples))) // Create a separate RNG for each goroutine
	inside := 0
	
	for i := 0; i < samples; i++ {
		x, y := r.Float64(), r.Float64()
		if x*x+y*y <= 1 {
			inside++
		}
	}
	
	return 4 * float64(inside) / float64(samples)
}

func main() {
	const (
		workers = 8
		samplesPerWorker = 1_000_000
	)
	
	var wg sync.WaitGroup
	
	// := is the short variable declaration operator - it declares and initializes variables in one step
	// make() allocates and initializes a slice (not an array) with the specified length and capacity
	results := make([]float64, workers)
	
	// Launch workers
	for i := 0; i < workers; i++ {
		wg.Add(1)
		
		// The "go" keyword launches a goroutine - a lightweight thread managed by the Go runtime
		// The function that follows executes concurrently with the rest of the program
		go func(id int) {
			// defer schedules a function call to run just before the function returns
			// This ensures wg.Done() is called even if the function panics
			defer wg.Done()
			
			results[id] = calculatePi(samplesPerWorker)
		}(i) // Passing 'i' as an argument to avoid capturing the loop variable by reference
	}
	
	// Wait for all goroutines to complete
	wg.Wait()
	
	// Calculate average
	sum := 0.0
	for _, result := range results {
		sum += result
	}
	
	pi := sum / float64(workers)
	fmt.Printf("π ≈ %.10f\n", pi)
}

Go can either run this directly with go run script.go or build a binary with go build script.go.

4 Likes

Go was built with simplicity, performance, and fast compile time in mind. However, if I were to build a language with these goals, it wouldn’t look remotely like Go. It would look much like Julia. It would be something designed to look similar to Python but with c++ style template on everything and jit compilation.

Go’s generic is a mess honestly, and it’s not really that type safe either, just more annoying. In Julia, you can chain generics and compile away all the type signatures.