#Executing System Commands With Golang

Elliot Forbes Elliot Forbes · Mar 7, 2026 · 6 min read

Last Updated - 7th March, 2026

In this tutorial, we are going to be taking a look at the os/exec package in the standard library and how we can use this to successfully execute system commands within our Go applications. We’ll cover modern Go 1.26 patterns and best practices for command execution, including timeout handling and security considerations.

Note - The official documentation for executing system commands can be found under the exec package: os/exec package.

Prerequisites

  • You will need Go version 1.11+ installed on your development machine.

Cross Compatibility Issues

Please note that some of these commands may not work on your operating system. If you are trying to write code that is compatible on multiple platforms then it would be wise to select commands that only feature on all platforms. If this is un-achievable then I recommend you add conditional logic to your program that executes a different system command depending on the system it’s executing on top of.

Checking Current Operating System

In order to check what operating system our code is running on we can use the runtime package and check the GOOS constant. This will return the operating system target:

    if runtime.GOOS == "windows" {
        fmt.Println("Can't Execute this on a windows machine")
    } else {
        execute()
    }

Note - The full list of GOOS variables can be found here: Sys Package.

Implementation

Note - I’m writing this tutorial on MacOS using commands that may not necessarily work on Windows machines.

Let’s have a look at how we can start executing some really simple commands such as ls and pwd using the os/exec package, and once we have the basics covered, we can move on to more advanced examples.

We’ll first of all need to import the 3 key packages for this example, the fmt, os/exec and the runtime package.

Once we’ve done this, we’ll define an execute() function which will attempt to execute some basic system commands.

package main

import (
    "fmt"
    "os/exec"
    "runtime"
)

func execute() {

    // here we perform the pwd command.
    // we can store the output of this in our out variable
    // and catch any errors in err
    out, err := exec.Command("ls").Output()

    // if there is an error with our execution
    // handle it here
    if err != nil {
        fmt.Printf("%s", err)
    }
    // as the out variable defined above is of type []byte we need to convert
    // this to a string or else we will see garbage printed out in our console
    // this is how we convert it to a string
    fmt.Println("Command Successfully Executed")
    output := string(out[:])
    fmt.Println(output)

    // let's try the pwd command here
    out, err = exec.Command("pwd").Output()
    if err != nil {
        fmt.Printf("%s", err)
    }
    fmt.Println("Command Successfully Executed")
    output = string(out[:])
    fmt.Println(output)
}

func main() {
    if runtime.GOOS == "windows" {
        fmt.Println("Can't Execute this on a windows machine")
    } else {
        execute()
    }
}

If we then attempt to run this, we should see the following:

$ go run main.go
Command Successfully Executed ## ls command
main.go

Command Successfully Executed ## pwd command
/Users/elliot/Documents/Projects/elliotforbes/...

As you can see, both of the commands are successfully executed and we’ve managed to capture the output from these commands and subsequently output them within the context of our own Go program.

Passing in Arguments

Awesome, we’ve managed to get some really simple commands running, but how do we go about passing in arguments to these commands?

For instance, say I wanted to do an ls -ltr as opposed to just a standard ls?

Thankfully, this is relatively easy, we just have to add these arguments to .Command() like so:

package main

import (
    "fmt"
    "os/exec"
    "runtime"
)

func execute() {
    out, err := exec.Command("ls", "-ltr").Output()
    if err != nil {
        fmt.Printf("%s", err)
    }
    fmt.Println("Command Successfully Executed")
    output := string(out[:])
    fmt.Println(output)
}

func main() {
    if runtime.GOOS == "windows" {
        fmt.Println("Can't Execute this on a windows machine")
    } else {
        execute()
    }
}

When we go to execute this again, we should see the output now successfully picking up our -ltr flags:

$ go run main.go
Command Successfully Executed
total 8
-rw-r--r--  1 elliot  staff  988  6 Dec 17:52 main.go

Note - .Command() is an example of a Variadic Function which takes in any number of trailing arguments, therefore, you can pass in as many arguments to your initial command as you desire.

Using exec.CommandContext for Timeouts and Cancellation

For production systems, it’s important to handle command execution timeouts and provide cancellation support. Go provides exec.CommandContext() which integrates with the standard context package.

When executing external commands, you should always set a timeout to prevent your application from hanging indefinitely if a command takes too long or becomes stuck. Using context.WithTimeout() or context.WithCancel() allows you to control command execution lifecycle.

Here’s an example with timeout handling:

package main

import (
    "context"
    "fmt"
    "os/exec"
    "time"
)

func execute() {
    // Create a context with a 5-second timeout
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    // Execute command with context
    cmd := exec.CommandContext(ctx, "ls", "-la")
    out, err := cmd.Output()

    if err != nil {
        fmt.Printf("Command failed: %v\n", err)
        return
    }

    fmt.Println(string(out))
}

func main() {
    execute()
}

This approach ensures that if the ls command takes longer than 5 seconds, the context will be canceled and the command will be terminated.

Security Considerations

When executing system commands, especially with user input, you must be vigilant about command injection vulnerabilities. Never concatenate user input directly into command strings or use shell interpreters unless absolutely necessary.

Always pass arguments separately to exec.Command() rather than building a shell command string. For example, use exec.Command("ls", userInput) instead of exec.Command("sh", "-c", "ls " + userInput). This approach prevents shell metacharacters in user input from being interpreted as commands. If you must use a shell, carefully validate and sanitize all user inputs, or consider using allowlists to restrict which commands users can execute.

Conclusion

So, in this tutorial, we looked at how we could leverage the os/exec package in Go to execute system commands within our Go programs. We covered modern patterns including context-based timeouts and important security considerations for production applications.

If you found this tutorial useful or wish to ask anything extra then please feel free to do so in the comments section below!

Note - If you want to keep up to date with the latest articles and updates on the site then please feel free to follow me on twitter: @Elliot_f