#Executing System Commands With Golang
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
Continue Learning
Parsing JSON files With Golang
In this tutorial we examine the encoding/json go package and how to parse JSON files.
Reading in Console Input in Golang
Learn how to read user input from the console in Go using bufio.Scanner, fmt.Scan, and os.Stdin.
Go JSON Tutorial
In this tutorial, we are going to cover everything you need when it comes to working with JSON in Go.
Build a Go Serverless App in 5 Minutes With Sst
In this video, we are going to look at what it takes to build a serverless application in Go in 5 minutes using SST.