How to make your Go Applications easy to port

Go is really well suited for cross-platform work. My main development environment is Windows, but I always deploy to Linux-based systems. Therefore I naturally try to avoid things that will get me into trouble.

gophers-crossplatform

 

My attitude towards cross-platform support is that if you consider yourself a serious developer, your code should at least compile on other platforms, because even though it may be impossible to use certain, or even all of your features, the users of your library may want to use your library only on some platforms.

I recently helped make a Windows version of a very nice backup program since I wanted to explore alternatives to zpaq, which is a very nice archiving application with journaling and focus on compression. During the port, there were a few things I noticed, that might be of help to others.

Minimize syscall (or sys) usage

We start with the obvious. The syscall package is different for each platform, so even though there is some overlap you are almost certainly getting into trouble. Of course there can be good reasons for using it, but before you do, be sure there isn’t something else that can do the same. If you do use syscall/sys, you can prepare for porting by only importing it into a separate file that has build similar build tags, for instance // +build darwin dragonfly freebsd linux netbsd openbsd solaris, and have an empty implementation as fallback, so you know it is possible to ‘fill in’ the missing parts on other platforms.

There is the github.com/golang/sys package that separates syscalls into packages for each platform. This will not solve this problem, so the same applies for this package.

Don’t rely on custom process signals

 

"no signal" by Πάνος Τσαλιγόπουλος
“no signal” by Πάνος Τσαλιγόπουλος

Process signals are very useful. There are many servers that use SIGHUP to reload configuration, SIGUSR2 to restart the service, etc.  Please be aware that these signals are not available on all platforms, so don’t make essential functionality depend on it. A webserver without the examples above will work just fine on Windows, even though it is missing some functionality. Of course the nicest thing would be to have an alternative for Windows, but as long as the server compiles and runs fine, I don’t think many people will object.

So if you for instance are implementing a task worker, you should not make the only way to kick it awake be sending a custom signal.

File system differences

Be aware that file systems are different.

  • Most other operating systems have case sensitive file system, but Windows doesn’t. My tip: Always use lower case.
  • Remember os.PathSeparator. Always use it, but it isn’t always the ONLY path separator. Windows can use both “/” and “\” seamlessly, so user-given and other paths  paths CAN be a mix of both.
  • Always use the filepath package, it may take a little more code, but you save yourself and others many headaches.
  • os.Remove and os.RemoveAll cannot delete Readonly files on Windows. This is a bug and should have been fixed long ago. Unfortunately: politics.
  • os.Link, os.Symlink, os.Chown, os.Lchown, os.Fchown all return errors on Windows. As a nice touch the error returned is only exported on Windows 🙁
  • os/user.Current will not work if a binary is crosscompiled.  See here.  Thanks @njcw.
  • Always close files before you modify/delete them.

The last point is actually the most common mistake I’ve seen, and it relates to a common way of handling files:


func example() (err error) {
    var f *os.File
    f, err = os.Create("myfile.txt")
    if err != nil {
         return
    }
    defer f.Close()

    err = f.write([]byte{"somedata"})
    if err != nil {
         return
    }

    // Do more work... 

    err = os.Remove("myfile.txt")
}

This is very subtle, but since this is a simple example, it is quickly obvious to see that we attempt to delete the file before we close it. The problem is that this works nicely on most systems, but will fail on Windows. Here is a more correct way to handle this:


func example() (err error) {
    var f *os.File
    f, err = os.Create("myfile.txt")
    if err != nil {
         return
    }

    defer func() {
        f.Close()
  
        err2 := os.Remove("myfile.txt")
        if err == nil && err2 != nil {
            err = err2
        } 
    }() 

    err = f.write([]byte{"somedata"}) 

    // Do more work
} 

As you can see handling close order isn’t trivial. If you choose to do two “defer”, remember the os.Remove must be deferred before the Close, since they are called in reverse order.

Here are a more detailed article explaining the differences between Windows and Linux file systems.

Use a translator if you use ANSI

"SEE YOU SPACE COWBOY logout screen" by Daniel Rehn
“SEE YOU SPACE COWBOY logout screen” by Daniel Rehn

 

Using console commands to format output can be a significant improvement. It can help make output easier to visually parse, by using colors, or you can do progress updates without having text scrolling.

If you use ANSI codes, you should always use a translation library. Start by putting it in, and save yourself some headaches later. Here are some I have found, not in any particular order.

If you have any particular recommendations or experiences, please put them in the comments below.

Don’t rely on symbolic links

Symbolic links are a nice feature. It allows for nice stuff, like being able to create a new version of a file and simply have a symbolic link  point to the newest file. However, on Windows symbolic links can only be created if your program is running with Admin rights. So even though it is a very nice feature, it is not something that you should base your core mechanics on.

Avoid cgo/executables, if at all possible

You should go out of your way to avoid cgo, since it is rather hard to set up a working environment on Windows. If you do use it, you are cutting off not only most Windows, but also App Engine users. If you provide an application, you must be prepared to deliver binaries for Windows.

The same goes for external executables. Try to minimize it for trivial tasks that are easily covered by libraries, and only use external commands for complex tasks.

Compile-time or Runtime?

Often, when dealing with OS specific, you have the dilemma between writing OS specific code. There may be some OS specific code needed to satisfy some constraint. Let’s take this example function; it does a lot of things, but one requirement is that it sets a file to read-only on non-Windows platforms. Let’s look at two implementations:


func example() {
    filename := "myfile.txt"
    fi, _ := os.Stat(f)

    // set file to readonly, except on Windows
    if runtime.GOOS != "windows" {
        os.Chmod(f, fi.Mode()&os.FileMode(^uint32(0222)))
    }
}

This does a runtime check, and executes code if it is not on Windows.

Compare that to:


func example() {
    filename := "myfile.txt"
    fi, _ := os.Stat(f)

    setNewFileMode(f, fi)
}

// example_unix.go
//+build !windows

// set file to readonly
func setNewFileMode(f string, fi os.FileInfo) error {
	return os.Chmod(f, fi.Mode()&os.FileMode(^uint32(0222)))
}

// example_windows.go:
// we don't set file to readonly on Windows
func setNewFileMode(f string, fi os.FileInfo) error {
	return nil
}

The last version is how many would say it must be done. I exaggerated the example to show that it might not always be the best solution. To me there may be cases where the first version is preferred, specifically, if this is the only place where the code is needed, then it is shorter, you don’t have to browse multiple source files to see what it does.

I have compiled a small table with the pros and cons I see with each solution:

Compile-time Pros Runtime Pros
  • Usually low to no overhead
  • OS specific code in separate files
  • Can use imports that doesn’t compile on all platforms
  • You can keep your code in one place
  • Some mistakes are discovered without cross-compiling
Compile-time Cons Runtime Cons
  • You may need to duplicate code
  • Can result in many small files, or a single file with lots of scattered functionality
  • You need to check multiple files to get an overview of code
  • You must use cross-compilation to check if your code compiles
  • Small overhead for the check
  • No good way see where your OS-specific code is
  • Structs/functions that aren’t OS independent cannot be used

 

In general I would recommend compile-time, with separate files unless you are writing tests. But in extreme examples you might prefer a runtime check.

Set up cross-platform CI tests

As a final thing, do set up crossplatform compilation test. This is one of the things I learned from restic, where they already had crosscompilation set up. When 1.5 is released, crossplatform compiling will be very easy, since it doesn’t require much setup.

Meanwhile, and for older Go versions, you might want to have a look at gox, which will help you set up crosscompilation. If you need more advanced features, you can have a look at goxc, for some more advanced features.

 

These were most of the small things I’ve learnt. If you have other tips or tricks, feel free to share them below.

Happy coding!

Flattr this!