Supercharging Flutter Apps with Go
— Flutter, Golang — 20 min read
I've been down a rabbit hole lately. I started writing an ebook, Flutter Security Guidelines, and while exploring cryptography, I wondered: Can I use Go's crypto libraries with Flutter? That curiosity led to this. We're going to look at how Flutter and Go can work together.
It's simpler than you might think!
📢 The fully documented source code from this post can be found on the GitHub.Why Flutter and Go
While Flutter is generally fantastic for creating UIs, Golang (or Go) truly shines in backend development. Go is all about simplicity and efficiency. Plus, it's known for its impressive speed! One of the standout features of Go is its ability to handle multiple tasks simultaneously, thanks to goroutines and channels. These elements make Go an excellent choice for building fast APIs, network services, and heavy computing tasks.
Go comes with many tested packages for various tasks: networking (net
), cryptography (crypto
), and input/output work.
The crypto
package is especially full of security-related libraries, which is why I picked Go for this experiment.
Go employs garbage collection and offers robust memory safety. Its automated memory management reduces the likelihood of memory leaks typically associated with manual memory handling in C/C++.
You may not have known this, but Go modules can run inside your Flutter app. This allows you to get the best of both worlds. You don't force one framework to do everything. Instead, you use each other's strengths. This way, you build better and faster mobile apps.
The main focus of this post will be on the gomobile
tool (made by the Go team).
It helps create native links for Android and iOS.
I'll create a simple Flutter project with basic cryptography functions (in Go), like random key generation, AES encryption, and decryption.
If you want to follow me on this journey, you should have at least a basic knowledge of Flutter and Dart. Knowing Go helps, but it's not a must. Go is easy to learn. If you already know C-like languages, you'll find it very familiar to code in it.
Linking Dart and Go
We need special tools to mix Go with Flutter code. This is where gomobile
steps in. It supports two main ways to use Go in mobile apps:
- Building all-Go native mobile apps: This is when you write mobile apps entirely in Go. The UI usually uses native UI kits or OpenGL. This is powerful but less useful for us, Flutter developers, as we already use Dart for app logic and UI.
- Making bindings from Go packages: This is often called building "SDK applications" or "libraries." We compile Go packages into native libraries. These libraries can be called from Java (for Android) and Objective-C/Swift (for iOS). We'll use this approach.
The gomobile
acts like a middle layer. It simplifies the complex job of connecting Go code with Android and iOS platforms.
Android uses JNI (Java Native Interface), while iOS uses Objective-C bridging. `gomobile' automates the necessary wrapper code creation.
Flutter talks to platform-specific native code via "platform channels." These channels let Dart code send messages to native code and get messages back. This native code is usually written in Kotlin on Android and Swift on iOS.
gomobile
creates native modules that Flutter can talk to.
When gomobile
processes a Go package, it creates an Android Archive (.aar file).
For iOS, it makes a Framework bundle (.xcframework).
These are standard native library types for their respective platforms.
The Flutter app, through its native Android and iOS parts, can then load these Go-powered libraries.
The existence of gomobile
and its ease of use are a big help.
Without it, many developers might avoid this combo. However, it has some limitations, which I will cover later.
Setting Up The Go Environment
Before writing any Go code, we must set up our Go environment. Platform-specific things like the Android NDK and Xcode Command Line Tools are also needed. If you set up the Flutter environment for mobile app development, these tools should already be on your machine.
First, the Go programming language. Go to the official Go downloads.
Get the installer for your operating system.
It's best to use the latest stable Go version (The gomobile
tool needs Go 1.16 or newer).
Install Go by following the installation steps for your platform. Then, open a terminal or command prompt and type:
go version
This command should show the Go version you installed.
Once Go is installed, we can install the gomobile
tool and set it up.
The following command downloads the gomobile
source code, compiles it, and puts the executable in our $GOPATH/bin
directory:
go install golang.org/x/mobile/cmd/gomobile@latest
After installation, run the initialization command:
gomobile init
This command sets your Go environment up for work. It compiles the Go standard libraries and runtime for mobile systems (like ARM), which can take a few minutes. If this command doesn't work, it often means problems with your installation or missing system dependencies.
Building The Crypto Core in Go
With the environment set up, we are finally ready to write some Go code. We'll create a separate folder for the Go module and then init the library:
md go_crypto_modulecd go_crypto_modulego mod init gocryptolib
When we create a Go package, we must follow gomobile
's rules for exportable types.
Functions and methods we wish to expose to the native part must have parameters and return types that gomobile
can map to Java/Kotlin and Objective-C/Swift types.
Note: In Go, any name (function, type, variable, struct field) that starts with a capital letter is exported. Only exported functions and methods from your Go package will be usable by
gomobile
.
The following Go types are supported for binding:
- Basic Types:
string
, signed numbers (int
,int8
,int16
,int32
,int64
), decimal numbers (float32
,float64
), andbool
. - Byte Slices:
[]byte
is well-supported. I will use it to pass crypto data. It's usually passed by reference, and the native side can change it. - Functions: Functions with supported input and output types.
- Structs: Structs whose exported fields are all supported types. Their exported methods must follow the function rules.
- Interfaces: Interfaces whose exported methods all have supported function types.
- Error Type: Functions can return an
error
as their last value.gomobile
maps this to native exceptions or error objects.
Some Go types are not directly supported or have limits:
- Unsigned numbers (e.g.,
uint32
,uint8
). - Complex groups like
map[string]MyStruct
or[]MyStruct
(slices of structs often need special ways to show their items one by one, or they need to be turned into[]byte
). time.Time
The API made in Go should be clean. It should be shaped explicitly for mobile use. It might act as a simple front for more complex Go logic inside.
Cryptography in Go
To simplify things, let's implement some basic functionalities like showing how to make secret keys, perform AES-GCM encryption/decryption, and generate secure random numbers.
GenerateAESKey
generates a random AES encryption key of the specified size using the crypto/rand
package. The key size must be one of the following valid AES key sizes:
- 16 bytes (128 bits)
- 24 bytes (192 bits)
- 32 bytes (256 bits)
func GenerateAESKey(keySize int) ([]byte, error) { if keySize != 16 && keySize != 24 && keySize != 32 { return nil, fmt.Errorf("invalid AES key size: %d bytes, must be 16, 24, or 32", keySize) } key := make([]byte, keySize) if _, err := rand.Read(key); err != nil { return nil, fmt.Errorf("failed to generate random key: %w", err) } return key, nil}
EncryptAESGCM
encrypts the given plaintext using AES encryption in Galois/Counter Mode (GCM).
The function uses a randomly generated nonce for each encryption operation to ensure security.
The GCM implementation determines the nonce size.
The maximum plaintext size is limited to 10MB to prevent potential attacks.
Note: For AES-GCM, every encryption with a key must use a new nonce. This usually means making a random nonce and adding it to the start of the ciphertext. The decryption process then removes this nonce before decrypting.
func EncryptAESGCM(key []byte, plaintext []byte) ([]byte, error) { if key == nil || plaintext == nil { return nil, errors.New("key or plaintext cannot be nil") } if len(key) != 16 && len(key) != 24 && len(key) != 32 { return nil, fmt.Errorf("invalid AES key size: %d bytes, must be 16, 24, or 32", len(key)) }
// Add size limit to prevent DoS attacks const maxPlaintextSize = 10 << 20 // 10MB limit if len(plaintext) > maxPlaintextSize { return nil, fmt.Errorf("plaintext exceeds maximum allowed size of %d bytes", maxPlaintextSize) }
block, err := aes.NewCipher(key) if err != nil { return nil, fmt.Errorf("failed to create AES cipher: %w", err) }
gcm, err := cipher.NewGCM(block) if err != nil { return nil, fmt.Errorf("failed to create GCM: %w", err) }
nonce := make([]byte, gcm.NonceSize()) if _, err = rand.Read(nonce); err != nil { return nil, fmt.Errorf("failed to generate nonce: %w", err) }
// Seal will append the ciphertext to the nonce and return it. ciphertext := gcm.Seal(nonce, nonce, plaintext, nil) return ciphertext, nil}
DecryptAESGCM
decrypts data encrypted using AES-GCM.
This function takes a key and a combined ciphertext with nonce as input, and returns the decrypted plaintext or an error if decryption fails.
func DecryptAESGCM(key []byte, ciphertextWithNonce []byte) ([]byte, error) { if key == nil || ciphertextWithNonce == nil { return nil, errors.New("key or ciphertextWithNonce cannot be nil") } block, err := aes.NewCipher(key) if err != nil { return nil, fmt.Errorf("failed to create AES cipher: %w", err) }
gcm, err := cipher.NewGCM(block) if err != nil { return nil, fmt.Errorf("failed to create GCM: %w", err) }
nonceSize := gcm.NonceSize() if len(ciphertextWithNonce) < nonceSize { return nil, errors.New("ciphertext too short to contain nonce") }
nonce, ciphertext := ciphertextWithNonce[:nonceSize], ciphertextWithNonce[nonceSize:]
// Add size limit for decryption result const maxPlaintextSize = 10 << 20 // 10MB limit if len(ciphertext) > maxPlaintextSize { return nil, fmt.Errorf("ciphertext too large, would result in plaintext exceeding %d bytes", maxPlaintextSize) }
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) if err != nil { // Use a generic error message to avoid leaking information return nil, errors.New("decryption failed") } return plaintext, nil}
GenerateSecureRandomBytes
generates a slice of cryptographically secure random bytes with the specified length.
This function is useful for generating secure random data for cryptographic purposes, such as keys, initialization vectors, or nonces.
func GenerateSecureRandomBytes(count int) ([]byte, error) { if count <= 0 { return nil, errors.New("count must be positive") } // Define an upper limit, e.g., 1MB const maxCount = 1 << 20 if count > maxCount { return nil, fmt.Errorf("count exceeds maximum allowed size of %d bytes", maxCount) } bytes := make([]byte, count) if _, err := rand.Read(bytes); err != nil { return nil, fmt.Errorf("failed to generate random bytes: %w", err) } return bytes, nil}
All functions return an error
as the second return value. This lets us signal failures.
gomobile
will change these into exceptions or error objects in the native mobile code.
Cryptography is complex underneath. However, Go's standard library provides a simple API for AES and GCM to write these functions correctly and securely.
Tip: For more insights, my upcoming ebook offers a comprehensive guide to cryptography in Flutter apps.
Binding Go to Native Library
Now, on to the next step: compiling the Go code into native libraries that Android and iOS can understand, or simply, binding.
The basic way to use gomobile bind
is:
gomobile bind [build flags] -target=android|ios [-o output_path] [package_path]
[build flags]
: Optional flags. They can change the build. For example,-v
gives more detailed output.-target=android|ios
: Tells which platform to build for.-o <output_path>
: Tells the output file name (for AARs). Or it's the directory name (for Frameworks).[package_path]
: The import path of the Go package to bind.
To make an Android library from our Go package, we use the -target=android
flag.
gomobile bind -v -target=android -o gocryptolib.aar gocryptolib
This assumes we are in the module's directory.
This command creates the gocryptolib.aar
file. This AAR file is a standard Android library that bundles:
- Compiled Go code as native shared libraries (
.so
files) for various Android systems (e.g.,arm64-v8a
,armeabi-v7a
,x86
,x86_64
). - Java wrapper classes. These give access to the exported Go functions and types.
gomobile
makes a Java class named after your Go package (e.g.,gocryptolib.Gocryptolib
). It might also make other classes for exported Go structs.
For iOS, the following command targets the ios
platform:
gomobile bind -v -target=ios -o GoCryptoLib.xcframework gocryptolib
This makes a GoCryptoLib.xcframework
directory. This framework bundle has:
- The compiled Go code. It's usually a static library.
- Objective-C header files (.h). These declare the interfaces to your exported Go functions and types.
- A module map that allows it to be used as a proper module in Swift and Objective-C.
Note: Building for iOS needs a macOS machine. Xcode and its command-line tools must be installed.
Making AARs and Frameworks requires familiarity with native Android (Gradle) and iOS (Xcode) build systems to add these files correctly.
This goes beyond the usual Flutter package management with pubspec.yaml
.
We all face these challenges in our Flutter journey, sooner or later. 😉
Flutter Integration
With the Go crypto library compiled into an Android AAR and an iOS Framework, the next step is to add these native modules to a Flutter app using Flutter's platform channels. Platform channels are the standard way for Dart and platform-specific native code to talk.
I presume you are already familiar with the concept of platform channels. If not, let me quickly explain: Platform channels let Flutter apps call native code, which runs on Android (Kotlin/Java) and iOS (Swift/Objective-C).
The native side can also return results. The most common type of request-response pattern is MethodChannel
. Messages are sent from Dart to the native side without blocking.
Responses are sent back the same way.
MethodChannel
uses StandardMessageCodec
by default, which supports turning simple JSON-like values into bytes and back.
These values include booleans, numbers, Strings, Lists, and Maps. Importantly for us, it supports Uint8List
(Dart).
This maps to byte[]
(Java/Kotlin) or NSData
/Data
(Objective-C/Swift).
This codec handles changing crypto data like keys and ciphertexts.
Each channel is named with a unique string, which acts like an identifier.
OK, now let's create the Flutter project the standard way, e.g.
flutter create go_flutter_crypto_app
Next, we need to copy the Go binding modules to their appropriate places:
- Android: The made
.aar
file will be in theandroid/app/libs/
directory. - iOS: The made
.xcframework
file will be added to the Xcode project inside theios/Runner
directory.
Android Integration
The following steps explain how to add the Go-made AAR to the Android part of your Flutter project and how to set up the platform channel.
- Make the
libs
directory insideandroid/app/
if it's not already there. - Copy the
gocryptolib.aar
intoandroid/app/libs/
.
Open android/app/build.gradle
and make these changes:
- Inside the
android { ... }
block, make sure yourminSdk
works with the API levelgomobile
targeted. (e.g.,gomobile build -androidapi 23
meansminSdk = 23
or higher). - Add the AAR as a dependency:
dependencies { // Add this line, gocryptolib is the name of your AAR file without .aar implementation(name: 'gocryptolib', ext: 'aar') // ... other dependencies}
Open android/build.gradle
and add the libs
directory to your repositories if it's not already there:
allprojects { repositories { google() mavenCentral() flatDir { // Add this dirs 'libs' } }}
Sync your Gradle files in Android Studio after these changes.
Open the MainActivity.kt
and add the integration code for the platform channel:
package com.example.go_flutter_crypto_app
import androidx.annotation.NonNullimport io.flutter.embedding.android.FlutterActivityimport io.flutter.embedding.engine.FlutterEngineimport io.flutter.plugin.common.MethodChannel
// Import the Go package (name matches your AAR/Go package)// The actual generated package will be 'gocryptolib' and the class 'Gocryptolib'import gocryptolib.Gocryptolib
class MainActivity : FlutterActivity() { override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) MethodChannel( flutterEngine.dartExecutor.binaryMessenger, "com.example.gocrypto/channel" ).setMethodCallHandler { call, result -> try { when (call.method) { "generateAESKey" -> { val keySize = call.argument<Int>("keySize") if (keySize == null) { result.error("INVALID_ARGS", "keySize is required", null) return@setMethodCallHandler } // Go 'int' often maps to 'long' in Java/Kotlin for gomobile val key = Gocryptolib.generateAESKey(keySize.toLong()) result.success(key) }
"encryptAESGCM" -> { val key = call.argument<ByteArray>("key") val plaintext = call.argument<ByteArray>("plaintext") if (key == null || plaintext == null) { result.error("INVALID_ARGS", "Key and plaintext are required", null) return@setMethodCallHandler } val ciphertext = Gocryptolib.encryptAESGCM(key, plaintext) result.success(ciphertext) }
"decryptAESGCM" -> { val key = call.argument<ByteArray>("key") val ciphertextWithNonce = call.argument<ByteArray>("ciphertextWithNonce") if (key == null || ciphertextWithNonce == null) { result.error( "INVALID_ARGS", "Key and ciphertextWithNonce are required", null ) return@setMethodCallHandler } val plaintext = Gocryptolib.decryptAESGCM(key, ciphertextWithNonce) result.success(plaintext) }
"generateSecureRandomBytes" -> { val count = call.argument<Int>("count") if (count == null) { result.error("INVALID_ARGS", "count is required", null) return@setMethodCallHandler } val bytes = Gocryptolib.generateSecureRandomBytes(count.toLong()) result.success(bytes) }
else -> result.notImplemented() } } catch (e: Exception) { // Catch errors from Go (which gomobile turns into Java exceptions) result.error( "GO_ERROR", "Error calling Go function ${call.method}: ${e.message}", e.toString() ) } } }}
iOS Integration
Adding to iOS means putting the premade framework into your Xcode project.
- Open the iOS project in Xcode:
ios/Runner.xcworkspace
. - In the Project Navigator, pick the "Runner" project.
- Pick the "Runner" target.
- Go to the "General" tab.
- Scroll down to "Frameworks, Libraries, and Embedded Content".
- Drag your
GoCryptoLib.xcframework
from Finder into this list. - Make sure "Embed & Sign" is picked for the framework.

Open the ios/Runner/AppDelegate.swift
file and add the integration part:
import Flutterimport UIKitimport GoCryptoLib // Import the Go framework (name matches your .xcframework without extension)
@main@objc class AppDelegate: FlutterAppDelegate { private let CHANNEL_NAME = "com.example.gocrypto/channel" // Consistent channel name override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { let controller : FlutterViewController = window?.rootViewController as! FlutterViewController let cryptoChannel = FlutterMethodChannel(name: CHANNEL_NAME, binaryMessenger: controller.binaryMessenger) cryptoChannel.setMethodCallHandler( { ( call: FlutterMethodCall, result: @escaping FlutterResult ) -> Void in // Helper to extract arguments as a dictionary let args = call.arguments as? [String: Any] do { switch call.method { case "generateAESKey": guard let keySize = args?["keySize"] as? Int else { result(FlutterError(code: "INVALID_ARGS", message: "keySize (Int) is required", details: nil)) return } var error: NSError? let keyData = GocryptolibGenerateAESKey(keySize, &error) if let error = error { result(FlutterError(code: "GO_ERROR", message: "Error generating AES key: \(error.localizedDescription)", details: nil)) return } result(keyData.map { FlutterStandardTypedData(bytes: $0) }) case "encryptAESGCM": guard let keyFlutter = args?["key"] as? FlutterStandardTypedData, let plaintextFlutter = args?["plaintext"] as? FlutterStandardTypedData else { result(FlutterError(code: "INVALID_ARGS", message: "key (Uint8List) and plaintext (Uint8List) are required", details: nil)) return } var error: NSError? let ciphertextData = GocryptolibEncryptAESGCM(keyFlutter.data, plaintextFlutter.data, &error) if let error = error { result(FlutterError(code: "GO_ERROR", message: "Error encrypting data: \(error.localizedDescription)", details: nil)) return } result(ciphertextData.map { FlutterStandardTypedData(bytes: $0) }) case "decryptAESGCM": guard let keyFlutter = args?["key"] as? FlutterStandardTypedData, let ciphertextWithNonceFlutter = args?["ciphertextWithNonce"] as? FlutterStandardTypedData else { result(FlutterError(code: "INVALID_ARGS", message: "key (Uint8List) and ciphertextWithNonce (Uint8List) are required", details: nil)) return } var error: NSError? let plaintextData = GocryptolibDecryptAESGCM(keyFlutter.data, ciphertextWithNonceFlutter.data, &error) if let error = error { result(FlutterError(code: "GO_ERROR", message: "Error decrypting data: \(error.localizedDescription)", details: nil)) return } result(plaintextData.map { FlutterStandardTypedData(bytes: $0) }) case "generateSecureRandomBytes": guard let count = args?["count"] as? Int else { result( FlutterError( code: "INVALID_ARGS", message: "count (Int) is required", details: nil ) ) return } var error: NSError? let bytesData = GocryptolibGenerateSecureRandomBytes(count, &error) if let error = error { result( FlutterError( code: "GO_ERROR", message: "Error generating random bytes: \(error.localizedDescription)", details: nil ) ) return } result(bytesData.map { FlutterStandardTypedData(bytes: $0) }) default: result(FlutterMethodNotImplemented) } } catch let error { // Catch errors from Go (which gomobile turns into Swift Error) result( FlutterError( code: "GO_ERROR", message: "Error calling Go function \(call.method): \(error.localizedDescription)", details: nil ) ) } }) GeneratedPluginRegistrant.register(with: self) return super.application( application, didFinishLaunchingWithOptions: launchOptions ) }}
Note: Swift's
do-try-catch
block handles errors thrown by the Go functions. These are then changed toFlutterError
.
Dart-Side Implementation
Now, we need a Dart service class. This will wrap the MethodChannel
logic and give the rest of the Flutter app a clean API.
We'll put the CryptoService
class in the lib/crypto_service.dart
file:
import 'dart:typed_data';
import 'package:flutter/services.dart';
class CryptoService { // Consistent channel name static const _platform = MethodChannel('com.example.gocrypto/channel');
Future<Uint8List?> generateAESKey(int keySize) async { try { final key = await _platform.invokeMethod<Uint8List>( 'generateAESKey', {'keySize': keySize}, ); return key; } on PlatformException catch (e) { print( 'Failed to generate AES key: ${e.message} (Code: ${e.code}, Details: ${e.details})', ); return null; } }
Future<Uint8List?> encryptAESGCM( Uint8List key, Uint8List plaintext, ) async { try { final args = {'key': key, 'plaintext': plaintext}; final ciphertext = await _platform.invokeMethod<Uint8List>( 'encryptAESGCM', args, ); return ciphertext; } on PlatformException catch (e) { print( 'Failed to encrypt: ${e.message} (Code: ${e.code}, Details: ${e.details})', ); return null; } }
Future<Uint8List?> decryptAESGCM( Uint8List key, Uint8List ciphertextWithNonce, ) async { try { final args = {'key': key, 'ciphertextWithNonce': ciphertextWithNonce}; final plaintext = await _platform.invokeMethod<Uint8List>('decryptAESGCM', args); return plaintext; } on PlatformException catch (e) { print( 'Failed to decrypt: ${e.message} (Code: ${e.code}, Details: ${e.details})', ); if (e.code == 'GO_ERROR' && e.message != null && e.message!.contains('authentication failed')) { print( 'Decryption specific error: Message authentication failed. Key might be incorrect or ciphertext tampered.', ); } return null; } }
Future<Uint8List?> generateSecureRandomBytes(int count) async { try { final bytes = await _platform.invokeMethod<Uint8List>( 'generateSecureRandomBytes', {'count': count}, ); return bytes; } on PlatformException catch (e) { print( 'Failed to generate random bytes: ${e.message} (Code: ${e.code}, Details: ${e.details})', ); return null; } }}
invokeMethod
calls the matching native method. Arguments are passed as a Map
while Uint8List
is used for byte array data.
Methods expect Uint8List
for results (byte arrays from Go).
PlatformException
is caught to handle errors sent from the native side (originated in Go).
Putting It All Together
Finally, the easiest part is creating a basic Flutter UI that will talk to the CryptoService
.
Please, check-out the full source code on GitHub. Embedding the full Flutter project is not approapriate for a blog post.

The UI provides buttons to perform the crypto actions. The results (keys, ciphertexts, random bytes) are shown in hex form, making them easy to read. Decrypted text is displayed directly.
And that was the entire flow! From Go-powered crypto to Flutter UI and back.
Final Thoughts
Bridging two different worlds, like Dart/Flutter and Go, might seem tricky. Error handling, data transfer, and API design must be handled carefully.
Calls via MethodChannel
are asynchronous from Dart's view. However, the native handler code (Kotlin/Swift) might block the platform's main thread. This happens if it runs a long Go operation synchronously. If a function might take a long time, move the native handler's call to a background thread/goroutine. It should return the result to Dart asynchronously. Go's goroutines can be used inside the Go library to do work simultaneously.
Debugging across platforms can be hard. Good logging is crucial during the development stage. On Android, use Android Studio's Logcat, while on iOS, use Xcode's console to see logs from Swift/Objective-C code. Logs from Go might also appear here. Standard Go debugging tools (like Delve, or simple fmt.Println
) can be used. Always test the Go package alone before binding!
Crypto functions bring their security-related troubles. Data passed from Dart to Go (and back) is sensitive. Avoid logging them. Clear them from memory when not needed, if you can. (Go's GC makes explicit clearing hard.)
Flutter💙Go
The reason for doing this specific Go<>Flutter combo is to get the best of both worlds. Flutter is excellent for building beautiful, fast, cross-platform UIs. Go is strong in raw computing speed, handles many tasks well, has memory safety, and has a rich standard library.
The ability to write core logic once in Go and use it on Android and iOS fits well with Flutter's fundamental principles. This can result in cross-platform apps that are easier to maintain and more consistent. You can build more advanced, secure, and high-performing mobile apps.
Mixing Flutter and Go requires some learning at first, but it opens up new possibilities for mobile app development.