Our startup ( Time Atlas Labs ) is still doing an app. We designed it from the start so that we can target it for both iOS and Android, and that necessitated backend that we could reuse. As we wanted deep integration with native APIs, the typical ‘React Native that looks like iOS’ wasn’t really tempting for us, and due to that we settled on stack which consists of (top down):
- SwiftUI UI, written in Swift
- Swift GRPC client on the device
- Go GRPC server on the device
- Go service on the device
The Go service on device can also interact with our cloud services, also using GRPC, but that is not subject of this post. What is, are ergonomics of writing software using Go and Swift. I am writing this relatively briefly, as most of this could use a lot longer description but I am mostly writing down my own thoughts and not really attempting to be particularly verbose, clear, or readable even. Sorry about that.
Go language and development thoughts
In general, Go is designed to be a small, understandable language, with semantics that mostly make sense unless you like robust software:
- nil pointers and crashes dereferencing to them leads to?
- struct zero values which make maintenance without ‘constructors’ pain in the ass?
- platform specific stdlib that does ‘something’ if it is not available for the platform you are targeting (the assumption is something UNIX-y, have fun looking at what some of the API calls do on e.g. Windows)
Go toolchain
The toolchain itself is brilliant:
go testwhich runs only tests that have changed? Yes, please.- Sane module system (after few iterations? Oh, yes..
- Good versioning system for language itself - you can say that ‘this is Go 1.18’ and it works with later Go 1 versions too.
Go language
The language on the other hand, is perhaps too simplistic. While generics are arguably nice to have, and I was unhappy about their lack in the past, but in our current project, we have only handful of uses of it:
- actor abstraction which definitely is nice with generics
- (ordered) set type, -//-
- some SQL glue that is nicer type-safe, than not
but that is it. Including all files in those packages (and not all of them actually use generics, there is also tests and other stuff), they are less than 1% of our total Go codebase (much of which is generated protobuf/GRPC code, but still). We probably use some external packages which also use generics, but I am too lazy to dig out the numbers for this.
There are twice as many lines in our codebase that just say if err != nil {. And then there is two more lines that most likely return nil, err and contain empty }. So most likely 6x as much of our codebase is boilerplate error handling as opposed to generics. Or more, as I checked just for the ^ pattern and not e.g. switch err or something else.
Similarly, I think pretty much every crash we have had in Go code has been null pointer dereference. Maintaining set of structs with pointers, and either having multiple constructors for it that are not in sync, or forgetting to update one and lacking test coverage for it, has been not great. Or maybe the struct has been received from e.g. JSON or protobuf, and we haven’t checked it for nil-ness before dereferencing it. Anyway, surprisingly many crashes for safe(ish) language.
Go summary
So I am giving the tooling 10/10, and language solid 7/10 mostly due to error handling and nil pointers.
Swift language and development thoughts
In Apple ecosystem, Objective C is on its way out (thank $DEITY), and Swift has been the new hotness for number of years. I think I looked at it first time in 2019, found it immature, and looked once more before settling on it in 2024 when we started working on Time Atlas Labs’ app(s).
Swift 6 as a language
For the record, I am talking about Swift 6 (latest as of now), some of the earlier iterations were quite bad in my opinion. Swift has matured a lot, and actually has reasonably good concurrency model although I would argue it is bit too complex for its own good:
- you can have threads
- you can have actors that are guaranteed to have only one code path synchronously running them at a same time (regardless of which thread they run in)
- you can have async code within threads/actors (.. sigh ..)
They had me at actors, but the async code feels bit overkill. Similarly the language has lots of modern nice syntactic glue all over its original design as basically pretty version of Objective C.
I actually like the language, although some execution semantics are bit to put it mildly (if you create a new Task (async code closure) within an actor, it inherits that running context UNTIL it suspends, and after that all bets are off which thread it is in). Similarly, the behavior of init in struct/classes combined with property wrappers for properties make for some quite ugly code (@State, and try doing something lazy with computation for struct properties.. ).
It also has structured enums, as well as nice unwrapping functionality (Go authors, are you listening?), and even exceptions handling of which is relatively straightforward unlike the Go error handling boilerplate the codebase fills up with. It also has properly checked optionals, which make nil pointer dereferences not happen.
So far this sounds lovely, and it actually is. I like the language.
The tooling on the other hand, not so much. I have used the compiler now for about year (Xcode 16.x mainly).
Xcode 16.x horror stories
This is probably the most funny message, that I got quite a lot of at some point:
Failed to produce diagnostic for expression; please submit a bug report (https://swift.org/contributing/#reporting-bugs)
No idea where it blew up, but it just did. Triage by removing changes until it compiles. Repeat. Fun.
Another good one:
error: cannot infer type of closure parameter 'X' without a type annotation
this happens when it fails to parse or process something within closures. It frequently had nothing to do with the particular closure parameter, but instead e.g. function call within closure had wrong number of arguments and it
SwiftUI has also some beautiful parts. While it is mostly happening synchronously in MainActor (special actor running in main event loop to avoid threading/concurrency issues), what would you guess what happens when you do this:
ViewThatFits {
View1
View2
}
For speed reasons, View1/View2 are actually built in separate thread(s) (apparently for speed reasons, this is not documented at all) and things that depend on UI things staying in MainActor blow up spectacularly. Sigh.
swift-frontend (of the compiler can sometimes get stuck. I have no idea what happened yesterday, but at two minutes I gave up:
mstenber 39560 100.0 2.4 412732672 803968 s001 R 4:36PM 2:03.23 /Applications/shared/Xcode-16.4.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift-frontend -frontend -c -filelist /tmp/TemporaryDirectory.yrf1DD/sources-1 -primary-file ...
Swift summary
As can be seen from the above, I was not impressed by the toolchain, so I give it 6. It has one of the worst compilers I have used recently both in terms of speed and in terms of detecting where errors actually happen. The language on the other hand gets 9/10, as I actually like it, with the exception of it perhaps trying to do too many things nowadays (the colored functions, e.g. async, make life much harder than it needs to be. Sync + threads is all you need almost always, and for the rare non-always case, you could use Go or some other language without colored functions but lightweight threads aka goroutines instead).
The hunt for ideal language continues
Modern computers are fast enough that developer experience is the thing I believe. Due to that, concise language (not too much typing, sane footprint in terms of features) with fast toolchain would be win-win for me. Unfortunately, both Go and Swift fail on these counts.
I guess if and when most of the code is written by language models as opposed to people, the verbosity of the language won’t matter as much, but compilation times and quality of error messages do (due to iteration speed), so I see Go as more futuristic language than Swift, at least as of now.