GO! Golang and Gomobile vs Android+Kotlin and IOS+Swift.
Pron, cons, and performance.
Recently I started looking for a cross-platform solution with a good set of features that could be used in mobile development. I don’t want to share UI: only business logic, networking, maybe work with DB.
Gomobile I find especially interesting. There are several benefits of using Go: it has a simple and lightweight gorutines (threads), widely used on the backend, has simple and readable code (comparing to C++), support of SQLite, has a Reactive library, supported in many IDEs and last but not least — good performance.
Let's dig more into Golang's performance itself. It is slightly faster than Java, but of course, it is slower than C++ or Swift. IMHO, all mobile cross-platform solutions are slower than Swift, except C++ (Android NDK), but considering problems that C++ can bring to our life (memory leaks, security, etc.) we don’t want to use it for a big project.
Another con that it is not as popular as Kotlin/Swift, at least on the StackOverflow.
When I started investigating Gomobile, I found out that it has two strategies you can follow to integrate it into the project:
*Writing all-Go native mobile applications.
*Writing SDK applications by generating bindings from a Go package and invoke them from Java/Kotlin (on Android) and Objective-C/Swift (on iOS)
Since I wanted to share business logic, I focused on the second solution — the library. But it has a few limitations and one of them worried me a lot:
Current limitations are listed below.
* Only a subset of Go types are currently supported.
* Language bindings have a performance overhead.
* There are a few limitations on how the exported APIs should look due to the limitations of the target language.
Performance overhead — what does it mean? I decided to do some benchmarking to test performance. For that, I decided to pick some heavy math computation — factorial. Actually, the task itself doesn’t matter, I just need to understand the difference between native execution and Go.
I used next code snipper for Go factorial function:
Here are results of the computation in the Go environment:
Golang Factorial: Elapsed Time for 10000 is 10
Golang Factorial: Elapsed Time for 50000 is 228
Golang Factorial: Elapsed Time for 100000 is 869
Golang Factorial: Elapsed Time for 500000 is 23978
Golang Factorial: Elapsed Time for 1000000 is 101687
Gomobile plugin allows us to generate .aar for Android and .framework for IOS. What’s interesting here is the size of the generated libraries for this small project (one class as you see above):
- Android: 5.5 MB (.arr and .jar with sources for binding)
- IOS: 15.7MB (.framework folder)
On each platform, I did two tests:
- Call Golang factorial library function and measure duration of execution in Go and on the caller side (Swift or Kotlin)
- Create and call the same factorial function in Swift/Kotlin to compare with Golang execution time
Input data: 10000, 50000, 100000, 500000, 1000000
Kotlin factorial function:
Execution of this function on the Android MainThread (high priority):
Kotlin: For 10000; elapsed Time: 108
Kotlin: For 50000; elapsed Time: 1226
Kotlin: For 100000; elapsed Time: 4070
Kotlin: For 500000; elapsed Time: 100010
Kotlin: For 1000000; elapsed Time: 432013
And this is binding with Goland. The first log from Golang followed by the same from Kotlin's side. It is much slower than standalone Golang execution but similar to Kotlin (out of many tests I run, usually it was slightly faster)
Golang Factorial: Elapsed Time for 10000 is 25
Kotlin: For 10000; elapsed Time: 26Golang Factorial: Elapsed Time for 50000 is 709
Kotlin: For 50000; elapsed Time: 709Golang Factorial: Elapsed Time for 100000 is 3019
Kotlin: For 100000; elapsed Time: 3019Golang Factorial: Elapsed Time for 500000 is 102840
Kotlin: For 500000; elapsed Time: 102841Golang Factorial: Elapsed Time for 1000000 is 437768
Kotlin: For 1000000; elapsed Time: 437768
Unfortunately, here I faced a problem with the creation of a factorial function due to language limitations (If you know how to bypass it, please let me know) I had to use BigInt library to implement this solution and performance was terrible.
I didn’t even finish all tests because it was very slow:
For 10000 time is 6.123746991157532
For 50000 time is 173.4142849445343
Here is the result of binding calls to Golang from Swift, which compared to the execution on the Android is much faster — it is even faster than the execution of standalone Go solution:
Golang Factorial: Elapsed Time for 10000 is 7
Swift: for 10000 time is 0.007142066955566406Golang Factorial: Elapsed Time for 50000 is 153
Swift: for 50000 time is 0.15248596668243408Golang Factorial: Elapsed Time for 100000 is 638
Swift: for 100000 time is 0.6384819746017456Golang Factorial: Elapsed Time for 500000 is 18540
Swift: for 500000 time is 18.540263056755066Golang Factorial: Elapsed Time for 1000000 is 85565
Swift: for 1000000 time is 85.56504201889038
Even if Gomobile has a performance overhead I don’t think it is critical. According to the results we got from this test, it will not impact your application performance significantly.
Note: This example is very simple and it doesn’t take into account device memory and CPU profiling. The proper way to test Gomobile performance is to create a real-life Mobile use case with multithreading, data serialization, DB storage and Rx.
- Golang: go1.13.5 darwin/amd64
- Android: Google Pixel 2, AS 3.6
- IOS device: iPhone XS, Xcode 11.3.1