Retrieve Location of macOS Device from Go

Participating in self-isolation is more fun when you have toys to play. As a fun weekend project, I wanted to look at how one accesses macOS Location Services and get the geographic location of the device from Go.

To obtain the geographic location of a device on macOS, we use Apple’s Core Location framework. The framework is part of the OS, but it requires writting Objective-C (or Swift). Thanks to Go’s cgo and because Objective-C is from the family of C languages, we can write a bridge between Objective-C and Go.

Full disclosure, I haven’t written a line in C for more than 15 years. And more to that, it’s the first time I actually wrote something in Objective-C. The code is for illustrative purposes only. If you can help making it better, please, file an issue or a PR to the example repo, or write me on Twitter.

The code might illustrate the example better than my English ;) Feel free to jump right into the repo on GitHub.

Part 1. Go

Let’s start with the Go code

// File location_manager_darwin.go

/* #import "location_manager_darwin.h" */
import "C"

// CurrentLocation retrives location of the device and returns it to the caller.
func CurrentLocation() (Location, error) {
    var cloc C.Location
    if ret := C.get_current_location(&cloc); ret != 0 {
        return Location{}, fmt.Errorf("failed to get location, code %d", ret)
    }

    loc := Location{
        // convert C Location to Go Location...
    }
    return loc, nil
}

Function CurrentLocation calls C function get_current_location, expecting it to either populate the C structure C.Location or return a non-zero error code.

get_current_location is a plain-C function, that acts as a glue layer between Objective-C and Go. The function and the C.Location type are described in the header file, which we import with #import "location_manager_darwin.h" in the cgo directive. We will implement the function later in this note. For now, the definition of the function from the header:

// File location_manager_darwin.h

// Location struct represents the location data
// from Apple's CLLocation object.
typedef struct _Location {
    CLLocationCoordinate2D coordinate;
    double altitude;
    double horizontalAccuracy;
    double verticalAccuracy;
} Location;

int get_current_location(Location *loc);

To compile the code we need two more cgo directives:

// File location_manager_darwin.go

/*
#cgo CFLAGS: -x objective-c -mmacosx-version-min=10.14
#cgo LDFLAGS: -framework CoreLocation -mmacosx-version-min=10.14

#import "location_manager_darwin.h"
import "C"
*/

cgo CFLAGS and cgo LDFLAGS define the behaviour of the compiler. Refer to Go’s documentation about cgo to learn more.

Part 2. Objective-C

As I said earlier, getting the user’s location requires writing some Objective-C.

To get the actual location we need an instance of Apple’s CLLocationManager class, whose delegate (CLLocationManagerDelegate) will receive the location events. For some degree, delegates in Objective-C are similar to interfaces in Go. That is, delegate is a class that must implement some methods, that will be invoked by the delegate’s owner.

To keep things simple, I implemented a class, LocationManager, that’s responsible for instantiating CLLocationManager and, at the same time, is the implementation of the delegate protocol:

// File location_manager_darwin.m

@interface LocationManager : NSObject <CLLocationManagerDelegate>
{
    CLLocationManager *manager;
}
@end

@implementation LocationManager

- (id)init {
    self = [super init];

    // create an instance of CLLocationManager
    manager = [[CLLocationManager alloc] init];
    // assign ourself as the delegate of the created instance
    manager.delegate = self;

    return self;
}

- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray<CLLocation *> *)locations {
    // implement CLLocationManagerDelegate protocol
}

- (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error {
    // implement CLLocationManagerDelegate protocol
}

CLLocationManager provides requestLocation method, that, as the name suggests, requests the user’s geolocation. The method is asynchronous; on the first call, the OS’s location service will fire a macOS’s modal dialogue, asking the user if they allow the application to access their location.

Below, method getCurrentLocation requests the geolocation and stops the invocation, waiting for events from OS’s location services to reach the delegate. The method then returns the location object (CLLocation) to the caller:

// File location_manager_darwin.m

@implementation LocationManager

- (id)init { ··· }

- (CLLocation *)getCurrentLocation {
    // request user’s current location
    [manager requestLocation];

    // start the run loop, waiting for the results from the delegate
    CFRunLoopRun();

    if (_errorCode != 0) {
        return nil;
    }
    // return the most recently retrieved user location
    return manager.location;
}

- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray<CLLocation *> *)locations {
    // we got the location: stop the run loop to give back the control to getCurrentLocation
    CFRunLoopStop(CFRunLoopGetCurrent());
}

- (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error {
    // we failed to get the location: store the error code and
    // stop the run loop to give back the control to getCurrentLocation
    _errorCode = error.code;
    CFRunLoopStop(CFRunLoopGetCurrent());
}

@end

In the comments to the code above, I mentioned “run loop”. This is a fairly big topic, and I’m definitely not an expert to discuss it. Refer to Apple’s documentation about CFRunLoopRun function and related topics to get the idea about asynchronous programming with Apple’s Foundation framework.

Part 3. C

Because we can’t instantiate and use Objective-C classes from cgo, we need a plain C function to be a bridge between Objective-C and Go. It’s time to look at the implementation of get_current_location function, we discussed in Part 1:

// File location_manager_darwin.m

int get_current_location(Location *loc) {
    // create an instance of our LocationManager class
    LocationManager *lm = [[LocationManager alloc] init];
    // obtain user’s location; the call blocks the thread
    CLLocation *clloc = [lm getCurrentLocation];

    if (lm.errorCode != 0) {
        return lm.errorCode;
    }

    // populate the resulting Location struct from Objective-C object
    loc->coordinate = clloc.coordinate;
    loc->altitude = clloc.altitude;

    return 0;
}

After days of browsing Apple’s documentation, SO and GitHub, I got the results I wanted. That’s how we use it in a Go code:

package main

func main() {
    loc, _ := CurrentLocation()
    fmt.Printf("got location: latitude %f, logitude %f\n", loc.Coordinate.Latitude, loc.Coordinate.Longitude)
}

When running the program in the terminal, macOS shows a popup window with the request to access the device’s location. The program then prints the obtained coordinates to the console (I redacted the real coordinates below with “xxxxxx”):

$ go run ./
got location: latitude 52.xxxxxx, logitude 13.xxxxxx

As I mentioned in the beginning, the code example is on GitHub https://github.com/narqo/playground-go/tree/master/osx-core-location.

If you have any comments or suggestions, reach out to me on Twitter.

Have fun and stay safe.