Golang Kill Process by PID: Kill, Signal, and CommandContext

Deepak Prasad
Tested on RHEL 10.2 with Go 1.27.0
Package os, os/exec, syscall (Go 1.27.0 standard library)
Applies to Any host with Go installed
Privilege Normal user for own processes; signaling other users' PIDs may require elevated privileges
Scope Kill by PID with os.FindProcess, Process.Signal vs Process.Kill, stop exec.Command children with Wait, exec.CommandContext timeouts and default cancellation, optional graceful escalation, Unix process groups, and common errors. Does not cover third-party process supervisors or name-based killing across platforms.
Related guides Getting started with Go
Golang monitor background process
Golang stop goroutine
Golang context
kill and pkill in Linux

Go terminates processes through os.Process and os/exec. The API you pick depends on whether you have a bare PID, a child you started, or a command that must stop when a context times out.

Situation API
Have a PID os.FindProcessSignal / Kill
Started process with exec.Command cmd.Process.Signal / Kill + cmd.Wait
Need timeout or cancellation exec.CommandContext

Process.Kill requests immediate termination. On Unix, Signal(syscall.SIGTERM) is the usual way to ask a program to shut down cleanly before you escalate. Neither call waits for the OS to finish tearing the process down; for children you start, you still arrange Wait. The sections below walk PID killing, signal choice, subprocess lifecycle, timeouts, and the cases where killing a parent leaves children running.


Kill a process by PID

Call os.FindProcess with the integer PID, then Kill or Signal. This is the direct answer to golang kill process by PID searches. Replace 12345 with the PID you want to stop.

go
package main

import (
	"fmt"
	"os"
)

func main() {
	pid := 12345 // replace with the target PID
	p, err := os.FindProcess(pid)
	if err != nil {
		fmt.Println(err)
		return
	}
	if err := p.Kill(); err != nil {
		fmt.Println(err)
	}
}
Output

On Unix, os.FindProcess succeeds for any positive PID value even when no process with that PID exists. The kernel is not queried until you call Signal or Kill, so a successful FindProcess does not prove the process is alive. Signaling a PID that already exited typically returns os: process already finished (see the errors table below).

When you need a Unix-specific existence or permission probe before sending a real signal, signal number zero does not terminate the process but still reports whether the PID is reachable:

go
if err := p.Signal(syscall.Signal(0)); err != nil {
    fmt.Println(err) // dead PID or permission denied
}

Label that pattern as Unix-specific. Prefer cmd.Process for children your program starts so you are not guessing PIDs. You need permission to signal another user's processes.


Process.Signal vs Process.Kill

Both APIs address one process. They differ in how forcefully the OS is asked to stop it.

API Typical purpose
p.Signal(sig) Send a particular OS signal
p.Signal(syscall.SIGTERM) Common Unix request for graceful termination
p.Kill() Immediate process termination

On Unix, Process.Kill provides SIGKILL-style immediate termination. The target cannot catch or ignore it. Other operating systems implement Kill with their native termination facilities, so do not assume every GOOS maps Kill to Unix SIGKILL.

Signal delivers a specific signal. On Unix, syscall.SIGTERM is what many daemons expect for cooperative shutdown (similar to the default kill in the shell). The target may ignore or handle it.

Both calls return an error from the signaling operation. Neither replaces Wait for a subprocess you started.

This program starts two sleep children on Linux so you can compare exit reasons:

go
package main

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

func main() {
	cmd1 := exec.Command("sleep", "60")
	if err := cmd1.Start(); err != nil {
		panic(err)
	}
	if err := cmd1.Process.Signal(syscall.SIGTERM); err != nil {
		fmt.Println("signal err:", err)
	}
	fmt.Println("SIGTERM path:", cmd1.Wait())

	cmd2 := exec.Command("sleep", "60")
	if err := cmd2.Start(); err != nil {
		panic(err)
	}
	time.Sleep(50 * time.Millisecond)
	if err := cmd2.Process.Kill(); err != nil {
		fmt.Println("kill err:", err)
	}
	fmt.Println("Kill path:", cmd2.Wait())
}
Output

Sample output:

output
SIGTERM path: signal: terminated
Kill path: signal: killed

Reserve Kill for hung or runaway processes. Try SIGTERM first when the child might flush buffers or close listeners.


Stop a process started with exec.Command

When you launch work with exec.Command and Start, cmd.Process holds the child PID. That is the usual path to kill an exec subprocess from the same Go program.

go
cmd := exec.Command("sleep", "100")
err := cmd.Start()

Later, stop the child cooperatively or forcefully:

go
cmd.Process.Signal(syscall.SIGTERM)
// or
cmd.Process.Kill()

Then reap the child:

go
err = cmd.Wait()

Process.Kill does not call Wait for you. After Start, arrange Wait (directly or in a goroutine) so Go obtains the final process state and releases associated process resources. Skipping Wait can leave zombie children on Unix.

go
package main

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

func main() {
	cmd := exec.Command("sleep", "100")
	if err := cmd.Start(); err != nil {
		panic(err)
	}
	fmt.Println("PID:", cmd.Process.Pid)

	time.Sleep(200 * time.Millisecond)
	if err := cmd.Process.Signal(syscall.SIGTERM); err != nil {
		fmt.Println("signal:", err)
	}
	fmt.Println("wait:", cmd.Wait())
}
Output

Sample output:

output
PID: 570197
wait: signal: terminated

On Linux, sleep exits on SIGTERM, so Wait reports signal: terminated. A Kill path would report signal: killed instead.


Stop a command after a timeout with exec.CommandContext

Wrap the command in a context.Context with a deadline or cancel function. exec.CommandContext ties subprocess lifetime to that context.

go
package main

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

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 80*time.Millisecond)
	defer cancel()

	cmd := exec.CommandContext(ctx, "sleep", "10")
	fmt.Println(cmd.Run())
}
Output

Sample output once the deadline passes:

output
signal: killed

By default, exec.CommandContext configures cancellation to call Process.Kill. It does not automatically send a graceful interrupt or SIGTERM, wait for a grace period, and then escalate. Treat CommandContext as a timeout or cancellation hook and still read Wait or Run errors so the child is reaped.

Use this pattern for HTTP handlers, jobs, and tests that must not hang forever.

Graceful cancellation before force kill

When you need signal-first behavior with a timeout, customize Cancel and WaitDelay on the same Cmd. Set both together: a custom Cancel alone leaves WaitDelay at zero, so Wait or Run can block indefinitely if the child ignores the cooperative signal.

go
package main

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

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 80*time.Millisecond)
	defer cancel()

	cmd := exec.CommandContext(ctx, "sleep", "10")
	cmd.Cancel = func() error {
		return cmd.Process.Signal(syscall.SIGTERM)
	}
	cmd.WaitDelay = 3 * time.Second
	fmt.Println(cmd.Run())
}
Output

Sample output on Linux when sleep exits on SIGTERM before the grace window expires:

output
signal: terminated

When the context expires, Cancel sends SIGTERM. If the process still has not exited after WaitDelay, os/exec calls Process.Kill. Together, custom Cancel and WaitDelay provide a concise graceful-escalation pattern with CommandContext.

Without a custom Cancel, you can still run an explicit start → SIGTERM → timer → KillWait sequence yourself, but Cancel plus WaitDelay covers the same lifecycle inside CommandContext. Label Unix SIGTERM behavior as Unix-specific when you ship cross-platform tools.


Does killing the parent kill its child processes?

No. Process.Kill kills only the process you address. Processes it started are not automatically terminated. Killing a parent shell does not reliably stop grandchildren; children may be reparented when the parent exits.

On Unix/Linux, one advanced pattern starts the child in its own process group so you can signal the whole group instead of a single PID. That helps when a wrapper script spawned additional workers you still need to stop together. Set SysProcAttr before Start:

go
cmd := exec.Command("sh", "-c", "your-command")
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}

if err := cmd.Start(); err != nil {
	log.Fatal(err)
}

pgid := cmd.Process.Pid
if err := syscall.Kill(-pgid, syscall.SIGTERM); err != nil {
	log.Println(err)
}

With Setpgid: true and the default Pgid: 0, the child becomes the leader of a new process group on Unix, so its PID identifies that group. A negative PID in syscall.Kill targets the whole group.

That is Unix/Linux-specific. Do not treat negative PID group kills as portable. For large or cross-platform process trees, use an external supervisor, container runtime, or job object appropriate to the OS. You can also track each child PID and signal them in order.


Common errors and platform differences

Symptom Explanation
os: process already finished PID already exited (for example after signaling a stale PID from FindProcess)
no such process PID no longer exists
operation not permitted Insufficient permission to signal that PID
FindProcess succeeded but process does not exist Expected on Unix; FindProcess does not validate liveness
Child remains after parent is killed Kill does not terminate descendants
Zombie or resource leak after Start Wait was never arranged
Expected CommandContext to send SIGTERM Default cancellation calls Process.Kill
Unix signal code fails on Windows POSIX signals are OS-specific; use build tags or separate files

Windows does not implement POSIX signals the same way Unix does. Process.Signal supports a smaller set there, while Process.Kill maps to native termination. Code that passes syscall.SIGUSR1 or assumes every Unix signal exists will misbehave on Windows unless you split by GOOS. For portable tools, document behavior per platform you ship and prefer CommandContext where its defaults are acceptable.


References


Summary

Killing a process from Go usually means choosing between Process.Signal for a cooperative stop and Process.Kill for immediate termination. Use os.FindProcess when you only have a PID, but remember that on Unix FindProcess does not confirm the process is still running until you signal it.

For children you start with exec.Command, call cmd.Process.Signal or cmd.Process.Kill, then cmd.Wait so the child is reaped and you read the real exit state. Process.Kill does not wait and does not stop descendant processes.

exec.CommandContext is the standard timeout pattern, but its default cancellation calls Process.Kill, not SIGTERM followed by a grace window. Set custom Cancel and WaitDelay together when you need cooperative shutdown before force kill.

Match signals and syscall.SysProcAttr to the GOOS you support. Reserve force kill for cases where cooperative shutdown failed or policy demands an immediate halt.


Frequently Asked Questions

1. How do I kill a process by PID in Go?

Call os.FindProcess with the integer PID, then Process.Signal for a cooperative stop or Process.Kill for immediate termination. On Unix, FindProcess does not verify the PID is alive until you signal or kill it.

2. Does os.FindProcess confirm that a PID is running?

Not on Unix. FindProcess returns a handle for any PID value; liveness and permission errors appear when you call Signal or Kill. A Unix-specific probe is Signal with signal number zero.

3. What is the difference between Process.Kill and Process.Signal?

Signal delivers a specific OS signal such as SIGTERM on Unix so the target can shut down cleanly if it handles that signal. Kill requests immediate termination and does not wait for exit.

4. Does Process.Kill wait for the process to exit?

No. Kill only requests termination. For a subprocess you started with os/exec, call cmd.Wait so Go reaps the child and you read the final exit state.

5. Does Process.Kill kill child processes too?

No. Kill and Signal apply only to the process you address. Descendants keep running unless you signal them separately, use a Unix process group, or rely on an external supervisor.

6. Does exec.CommandContext send SIGTERM before killing the process?

No by default. CommandContext sets Cmd.Cancel to call Process.Kill when the context ends. Customize Cancel and WaitDelay when you want a cooperative signal first or a bounded wait after cancellation.

7. How do I kill a process by name in Go?

The standard library has no portable find-by-name-and-kill API. Keep the os.Process from a subprocess you started, or resolve the PID with OS-specific tools outside Go before calling FindProcess.
Tuan Nguyen

Data Scientist

Proficient in Golang, Python, Java, MongoDB, Selenium, Spring Boot, Kubernetes, Scrapy, API development, Docker, Data Scraping, PrimeFaces, Linux, Data Structures, and Data Mining. With expertise spanning these technologies, he develops robust solutions and implements efficient data processing and management strategies across various projects and platforms.

  • Go (programming language)
  • Python (programming language)
  • Java (programming language)
  • MongoDB
  • Kubernetes