Jan 15, 2024 #tech
5 min read

Go vs Scala: Two Philosophies of Parallelism

Go Scala

When building concurrent systems, the choice of language shapes not just your code, but your entire mental model. Go and Scala represent two fundamentally different philosophies: velocity versus expressivity.

I recently built the same tool in both languages, a parallel HTTP endpoint checker, and the experience revealed fascinating trade-offs worth exploring.

The Task

Both implementations verify a list of web endpoints, checking whether paths are correctly configured as public or private. Simple enough, but the approaches couldn't be more different.

graph LR A[Endpoints] --> B{Parallel} B --> C[Public] B --> D[Private] C --> E[200 OK] D --> F[401/403] E --> G[Results] F --> G

The Implementations

Go: Velocity Through Simplicity

The Go concurrency model is elegantly minimal. Goroutines are lightweight threads managed by the runtime, and launching one requires just the go keyword:

type Config struct {
BaseURL string
PublicPath []string
PrivatePath []string
}
var wg sync.WaitGroup
wg.Add(len(config.PublicPath))
for _, path := range config.PublicPath {
go func(p string) {
defer wg.Done()
checkEndpoint(p, true)
}(path)
}
wg.Wait()
func checkEndpoint(path string, expectPublic bool) {
resp, err := http.Get(config.BaseURL + path)
if err != nil {
log.Printf("Error: %s", err)
return
}
defer resp.Body.Close()
if expectPublic && resp.StatusCode == 200 {
log.Printf("✓ %s is public", path)
} else if !expectPublic && resp.StatusCode == 401 {
log.Printf("✓ %s is private", path)
}
}

The sync.WaitGroup coordinates completion: increment before launching, decrement when done, wait for zero. That's it.

What makes Go remarkable is what it doesn't require: no external dependencies, no complex type signatures, no build tool configuration. The standard library handles HTTP, synchronisation, and everything else.

The Go runtime scheduler automatically distributes goroutines across CPU cores. You don't manage threads; you describe work. This abstraction lets developers focus on program structure rather than low-level threading.

Scala: Expressivity Through Composition

Scala with Cats Effect takes a radically different approach. Instead of imperative coordination, you compose effects:

def checkStatusCode(client: Client[IO], url: String): IO[Status] =
IO.fromEither(Uri.fromString(url))
.flatMap(uri => client.run(Request[IO](GET, uri = uri))
.use(resp => IO.pure(resp.status)))
.handleError(_ => Status.ServiceUnavailable)
def isPublic(config: Config, path: String)
(client: Client[IO]): IO[Endpoint] =
checkStatusCode(client, config.baseUrl + path).map { status =>
if (status == Status.Ok)
Endpoint.CheckSuccess(path, status.code)
else
Endpoint.CheckError(path, status.code, expected = 200)
}
val privatePathValidation = config.privatePath
.parTraverse(path => isPrivate(config, path)(client))
val publicPathValidation = config.publicPath
.parTraverse(path => isPublic(config, path)(client))
(privatePathValidation, publicPathValidation)
.parMapN(_ ++ _)

The parTraverse function applies an effectful operation to each element in parallel, while parMapN combines results from independent parallel computations. Error handling, resource management, and cancellation are all handled by the type system.

The Numbers

Performance Comparison

Running both on CI revealed the velocity gap:

5s Go Build + Run
51s Scala Build + Run
10x Speed Difference
Implementation Dependencies Build + Run Time
Go 0 external 5 seconds
Scala 12 libraries 51 seconds
Scala (cached) 12 libraries 39 seconds

Go is 10x faster. Zero dependencies means no download time, no resolution, no compilation of external libraries. It just builds and runs.

When to Choose

When Each Wins

Go excels when you need fast iteration cycles and deployment simplicity. It's perfect for CLI tools, microservices, and DevOps tooling where a single binary matters. Teams with mixed experience levels can be productive quickly thanks to Go's deliberately simple design.

Scala shines in complex distributed systems where type safety prevents entire categories of bugs. When you need sophisticated error handling, resource management, and your domain benefits from functional composition, Scala's expressivity pays dividends. It's also the natural choice for Big Data workloads with Spark or Flink, or when you need deep JVM ecosystem access.

The Trade-off

Aspect Go Scala
Learning curve Weekend Months
Feedback loop Fast Slow
Type safety Basic Advanced
Abstraction Limited Powerful
Deployment Single binary JVM + deps
Ecosystem Standard lib Rich libraries

Conclusion

Neither is universally better. Go wins on velocity; Scala wins on expressivity. The right choice depends on what you're building, who's building it, and how long you'll maintain it.

For a quick endpoint checker? Go, every time. For a distributed event processing pipeline? Scala's type safety becomes invaluable.

The best engineers know both philosophies and reach for the right tool.

The Pragmatic Engineer
esc