macOS Hardware Detection with Go

Getting detailed hardware information from macOS requires working with various system utilities like sysctl
, system_profiler
, and vm_stat
. Each of these tools outputs data in different formats, making it challenging to build a unified interface for hardware detection.
This tutorial demonstrates building a Go CLI tool that extracts comprehensive hardware details from macOS systems. We’ll work with the raw output from macOS-specific commands, parse their varied formats, and present the information in a structured way.
The implementation leverages Go’s ability to execute system commands and parse their text output. We’ll handle the complexities of different command formats - from sysctl
’s key-value pairs to system_profiler
’s nested text structure. The tool uses concurrent execution to gather different types of hardware information simultaneously, making it both fast and efficient.
You’ll learn how to reliably parse command output, handle edge cases in system data, and structure Go code for robust hardware detection on macOS systems.
2. Project Setup and Data Structures
Before diving into hardware detection, we need to establish the foundation with proper data structures that represent different hardware components. Each struct corresponds to a specific category of system information we’ll be collecting.
package main
import (
"bufio"
"fmt"
"os"
"os/exec"
"regexp"
"runtime"
"strconv"
"strings"
"sync"
"time"
)
The import list covers everything we’ll need: os/exec
for running system commands, regexp
for parsing complex output patterns, bufio
for reading command output line by line, and sync
for concurrent execution.
Our data structures mirror the hardware components we’re targeting:
type OSInfo struct {
Name string
Version string
Kernel string
Hostname string
Architecture string
GoVersion string
NumCPU int
}
type CPUInfo struct {
Model string
Cores int
Threads int
Frequency string
VendorID string
ModelName string
L2Cache string
L3Cache string
}
type MemoryInfo struct {
TotalRAM string
UsedRAM string
AvailableRAM string
TotalSwap string
UsedSwap string
FreeSwap string
}
type DiskInfo struct {
FileSystem string
Size string
Used string
Available string
UsePercent string
MountedOn string
}
type NetworkInterface struct {
Name string
MACAddress string
IPAddresses []string
MTU string
}
type GPUInfo struct {
Vendor string
Model string
VRAM string
}
Each struct is designed to hold the specific information we can extract from macOS commands. The DiskInfo
struct maps directly to df
command output, while NetworkInterface
accommodates the multiple IP addresses that a single interface might have. The GPUInfo
struct handles the varying levels of detail that system_profiler
provides for different GPU types.
3. Helper Functions
The core of our hardware detection relies on several utility functions that handle command execution, text parsing, and data formatting. These functions abstract away the complexity of working with system command outputs.
func runCommand(name string, args ...string) (string, error) {
cmd := exec.Command(name, args...)
output, err := cmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf("command '%s %s' failed: %w\nOutput: %s", name, strings.Join(args, " "), err, string(output))
}
return strings.TrimSpace(string(output)), nil
}
The runCommand
function executes system commands and returns their output as a trimmed string. Using CombinedOutput()
captures both stdout and stderr, which is crucial when commands might write error information to stderr while still producing useful output. The error message includes the full command and its output for debugging purposes.
func parseLine(line, key string) string {
if strings.HasPrefix(line, key) {
parts := strings.SplitN(line, ":", 2)
if len(parts) == 2 {
return strings.TrimSpace(parts[1])
}
}
return ""
}
Many macOS commands output data in “key: value” format. The parseLine
function extracts values from these lines by splitting on the colon and returning the trimmed value portion. This pattern appears frequently when parsing /proc/cpuinfo
style outputs and sysctl
results.
func formatBytes(bytes uint64) string {
const unit = 1024
if bytes < unit {
return fmt.Sprintf("%d B", bytes)
}
div, exp := int64(unit), 0
for n := bytes / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %ciB", float64(bytes)/float64(div), "KMGTPE"[exp])
}
The formatBytes
function converts raw byte values into human-readable format using binary prefixes (KiB, MiB, GiB). This is essential since many system commands return memory and disk sizes as raw numbers, and we need consistent formatting across different data sources.
func isLikelyRawNumeric(s string) bool {
if s == "" {
return false
}
_, err := strconv.ParseUint(s, 10, 64)
return err == nil
}
This validation function helps determine whether a string represents a raw numeric value, which is particularly useful when parsing df
output that might return either human-readable sizes or raw block counts depending on the flags used.
4. Operating System Information
The getOSInfo()
function gathers basic system details using macOS-specific commands. This information provides the foundation for understanding what system we’re running on and its basic characteristics.
func getOSInfo() OSInfo {
hostname, _ := os.Hostname()
kernel := "N/A"
osName := runtime.GOOS
osVersion := "N/A"
We start with Go’s built-in functions to get the hostname and GOOS value, then enhance this with macOS-specific details.
if runtime.GOOS == "darwin" {
osNameFull, err := runCommand("sw_vers", "-productName")
if err == nil {
osName = osNameFull
}
osVer, err := runCommand("sw_vers", "-productVersion")
if err == nil {
osVersion = osVer
}
kern, err := runCommand("uname", "-r")
if err == nil {
kernel = kern
}
}
The sw_vers
command is macOS-specific and provides clean, parseable output for the product name and version. For example, sw_vers -productName
returns “macOS” while sw_vers -productVersion
returns something like “13.2.1”. The uname -r
command gives us the Darwin kernel version.
return OSInfo{
Name: osName,
Version: osVersion,
Kernel: kernel,
Hostname: hostname,
Architecture: runtime.GOARCH,
GoVersion: runtime.Version(),
NumCPU: runtime.NumCPU()
}
The function returns a complete OSInfo
struct combining both Go runtime information and macOS system details. The runtime.GOARCH
gives us the processor architecture (like “amd64” or “arm64”), while runtime.NumCPU()
provides the number of logical CPUs visible to the Go runtime.
This approach handles cases where system commands might fail gracefully by falling back to “N/A” values, ensuring the function always returns usable data even when some commands are unavailable or fail to execute.
5. CPU Information
The getCPUInfo()
function extracts detailed processor information using macOS’s sysctl
command, which provides access to kernel state and hardware details through a hierarchical namespace.
func getCPUInfo() CPUInfo {
info := CPUInfo{Threads: runtime.NumCPU()}
We initialize with the thread count from Go’s runtime as a fallback, then enhance it with macOS-specific details.
if runtime.GOOS == "darwin" {
model, _ := runCommand("sysctl", "-n", "machdep.cpu.brand_string")
info.Model = model
if c, e := runCommand("sysctl", "-n", "hw.physicalcpu"); e == nil {
if v, e2 := strconv.Atoi(c); e2 == nil {
info.Cores = v
}
}
if t, e := runCommand("sysctl", "-n", "hw.logicalcpu"); e == nil {
if v, e2 := strconv.Atoi(t); e2 == nil {
info.Threads = v
}
}
The sysctl -n
flag returns just the value without the key name. machdep.cpu.brand_string
gives us the full CPU model name like “Apple M1 Pro” or “Intel® Core™ i7-9750H CPU @ 2.60GHz”. The distinction between hw.physicalcpu
(actual cores) and hw.logicalcpu
(threads including hyperthreading) is crucial for understanding CPU topology.
freqB, e := runCommand("sysctl", "-n", "hw.cpufrequency_max")
if e != nil || freqB == "" {
freqB, e = runCommand("sysctl", "-n", "hw.cpufrequency")
}
if e == nil {
if fq, e2 := strconv.ParseUint(freqB, 10, 64); e2 == nil {
info.Frequency = fmt.Sprintf("%.2f GHz", float64(fq)/1000000000.0)
}
}
CPU frequency detection tries hw.cpufrequency_max
first, falling back to hw.cpufrequency
. The raw value comes in Hz, so we convert to GHz for readability. On Apple Silicon Macs, these values might not be available due to dynamic frequency scaling.
if strings.Contains(info.Model, "Intel") {
info.VendorID = "GenuineIntel"
} else if strings.Contains(info.Model, "AMD") {
info.VendorID = "AuthenticAMD"
} else if strings.Contains(info.Model, "Apple") {
info.VendorID = "Apple"
}
Since macOS doesn’t expose vendor IDs through sysctl like Linux does, we parse the model string to determine the vendor. This handles Intel, AMD, and Apple Silicon processors.
l2B, e := runCommand("sysctl", "-n", "hw.l2cachesizepercore")
if e != nil || l2B == "" {
l2B, e = runCommand("sysctl", "-n", "hw.l2cachesize")
}
if e == nil {
if l2, e2 := strconv.ParseUint(l2B, 10, 64); e2 == nil {
info.L2Cache = formatBytes(l2)
}
}
l3B, e := runCommand("sysctl", "-n", "hw.l3cachesize")
if e == nil {
if l3, e2 := strconv.ParseUint(l3B, 10, 64); e2 == nil {
info.L3Cache = formatBytes(l3)
}
}
}
Cache information attempts per-core L2 cache size first, then falls back to total L2 cache. The raw values are in bytes and get formatted using our formatBytes
helper function. L3 cache is typically shared across cores, so there’s usually only one sysctl entry for it.
The function includes fallback logic to ensure we always return reasonable values even when some sysctl queries fail, which can happen on different Mac models or macOS versions.
6. Memory Information
The getMemoryInfo()
function collects RAM and swap usage data using macOS-specific commands. Memory information on macOS requires combining data from multiple sources due to how the system manages memory differently than traditional Unix systems.
func getMemoryInfo() MemoryInfo {
memInfo := MemoryInfo{}
if runtime.GOOS == "darwin" {
var totMemB uint64
totMemS, e := runCommand("sysctl", "-n", "hw.memsize")
if e == nil {
if t, e2 := strconv.ParseUint(totMemS, 10, 64); e2 == nil {
totMemB = t
memInfo.TotalRAM = formatBytes(totMemB)
}
}
Total memory is straightforward - hw.memsize
returns the physical RAM in bytes. This value represents the actual hardware memory installed in the system.
vmStatO, e := runCommand("vm_stat")
if e == nil && totMemB > 0 {
var pgSz uint64 = 16384
pgSzS, psE := runCommand("sysctl", "-n", "hw.pagesize")
if psE == nil {
if ps, ce := strconv.ParseUint(pgSzS, 10, 64); ce == nil {
pgSz = ps
}
}
The vm_stat
command provides memory statistics in pages, so we need the page size to convert to bytes. On Apple Silicon Macs, the page size is typically 16KB, while Intel Macs use 4KB pages. We default to 16KB and query the actual value.
var actC, inactC, wireC, comprC, freeC uint64
scan := bufio.NewScanner(strings.NewReader(vmStatO))
for scan.Scan() {
ln := scan.Text()
flds := strings.Fields(ln)
if len(flds) < 2 {
continue
}
vS := strings.TrimRight(flds[len(flds)-1], ".")
v, _ := strconv.ParseUint(vS, 10, 64)
switch {
case strings.HasPrefix(ln, "Pages active:"):
actC = v
case strings.HasPrefix(ln, "Pages inactive:"):
inactC = v
case strings.HasPrefix(ln, "Pages wired down:"):
wireC = v
case strings.HasPrefix(ln, "Pages occupied by compressor:"):
comprC = v
case strings.HasPrefix(ln, "Pages free:"):
freeC = v
}
}
macOS memory management uses several categories: active pages (recently used), inactive pages (not recently used but cached), wired pages (kernel memory that can’t be swapped), compressed pages (memory compressed to save space), and free pages. Each line in vm_stat
output ends with a period that we need to trim.
usedB := (actC + wireC + comprC) * pgSz
memInfo.UsedRAM = formatBytes(usedB)
if totMemB >= usedB {
memInfo.AvailableRAM = formatBytes(totMemB - usedB)
} else {
memInfo.AvailableRAM = formatBytes((freeC + inactC) * pgSz)
}
Used memory includes active, wired, and compressed pages since these represent memory actively consumed by the system and applications. Available memory is calculated as total minus used, with a fallback calculation using free and inactive pages if the math doesn’t work out.
swUseO, e := runCommand("sysctl", "-n", "vm.swapusage")
if e == nil {
re := regexp.MustCompile(`total\s*=\s*([\d\.]+)(\w+)\s*used\s*=\s*([\d\.]+)(\w+)\s*free\s*=\s*([\d\.]+)(\w+)`)
m := re.FindStringSubmatch(swUseO)
if len(m) == 7 {
memInfo.TotalSwap = m[1] + m[2]
memInfo.UsedSwap = m[3] + m[4]
memInfo.FreeSwap = m[5] + m[6]
}
}
}
Swap information comes from vm.swapusage
, which outputs a formatted string like “total = 2.00G used = 1.25G free = 768.00M”. We use a regex to extract the numeric values and units, keeping them as formatted strings since they’re already human-readable.
The function ensures all fields have values by setting empty strings to “N/A” at the end, providing consistent output even when some commands fail.
7. Disk Information
The getDiskInfo()
function retrieves filesystem information using the df
command with multiple fallback strategies to handle different macOS configurations and command availability.
func getDiskInfo() []DiskInfo {
var disks []DiskInfo
var out string
var err error
commandToUse := []string{"df", "-hP"}
humanReadableExpected := true
out, err = runCommand(commandToUse[0], commandToUse[1:]...)
if err != nil {
commandToUse = []string{"df", "-Pk"}
out, err = runCommand(commandToUse[0], commandToUse[1:]...)
if err != nil {
commandToUse = []string{"df"}
humanReadableExpected = false
out, err = runCommand(commandToUse[0], commandToUse[1:]...)
if err != nil {
return disks
}
}
}
The function tries three different df
command variations. First, it attempts df -hP
which provides human-readable output (-h
) in POSIX format (-P
ensures each filesystem appears on one line). If that fails, it tries df -Pk
for kilobyte blocks in POSIX format. Finally, it falls back to plain df
with raw block output.
lines := strings.Split(out, "\n")
if len(lines) > 1 {
for _, line := range lines[1:] {
fields := strings.Fields(line)
if len(fields) < 6 {
continue
}
disk := DiskInfo{
FileSystem: fields[0],
Size: fields[1],
Used: fields[2],
Available: fields[3],
UsePercent: fields[4],
MountedOn: strings.Join(fields[5:], " "),
}
We skip the header line and parse each filesystem entry. The df
output format is consistent: filesystem name, total size, used space, available space, usage percentage, and mount point. Mount points with spaces in their names require joining all remaining fields.
needsConversion := !humanReadableExpected
if humanReadableExpected && isLikelyRawNumeric(disk.Size) {
needsConversion = true
}
if needsConversion {
var blockSizeMultiplier uint64 = 512
if commandToUse[0] == "df" && len(commandToUse) > 1 && commandToUse[1] == "-Pk" {
blockSizeMultiplier = 1024
}
if sizeVal, sErr := strconv.ParseUint(disk.Size, 10, 64); sErr == nil {
disk.Size = formatBytes(sizeVal * blockSizeMultiplier)
}
if usedVal, uErr := strconv.ParseUint(disk.Used, 10, 64); uErr == nil {
disk.Used = formatBytes(usedVal * blockSizeMultiplier)
}
if availVal, aErr := strconv.ParseUint(disk.Available, 10, 64); aErr == nil {
disk.Available = formatBytes(availVal * blockSizeMultiplier)
}
}
disks = append(disks, disk)
}
}
return disks
When we don’t expect human-readable output, or when the supposedly human-readable output contains raw numbers, we need to convert block counts to bytes. The df -Pk
command uses 1024-byte blocks, while plain df
typically uses 512-byte blocks on macOS. Our isLikelyRawNumeric()
helper detects when the -h
flag didn’t work as expected.
This multi-layered approach ensures we get filesystem information regardless of which df
options are supported on the specific macOS version, and handles the complexity of different output formats by normalizing everything to human-readable byte values.
8. Network Interfaces
The getNetworkInterfaces()
function extracts network interface information using ifconfig
, parsing its complex output format to identify active interfaces with their configuration details.
func getNetworkInterfaces() []NetworkInterface {
var interfaces []NetworkInterface
if runtime.GOOS == "darwin" {
out, e := runCommand("ifconfig", "-a")
if e != nil {
return interfaces
}
The ifconfig -a
command shows all network interfaces, including inactive ones. We’ll filter for active interfaces during parsing.
mcDRx := regexp.MustCompile(`ether\s+([0-9a-fA-F:]{17})`)
ipDRx := regexp.MustCompile(`inet\s+([0-9\.]+)\s+netmask`)
ip6DRx := regexp.MustCompile(`inet6\s+([0-9a-fA-F:]+)%?\w*\s+prefixlen\s+(\d+)`)
mtuDRx := regexp.MustCompile(`mtu\s+(\d+)`)
stActRx := regexp.MustCompile(`status:\s+active`)
These regex patterns match the different data elements in ifconfig
output. The MAC address pattern looks for the “ether” keyword followed by a colon-separated hex string. IPv4 addresses appear after “inet” with netmask information. IPv6 addresses include prefix length and sometimes zone identifiers (the %
character). MTU and status information help determine interface characteristics.
var curN, curMtu, curMac string
var curIps []string
isAct := false
scan := bufio.NewScanner(strings.NewReader(out))
for scan.Scan() {
ln := scan.Text()
if !strings.HasPrefix(ln, "\t") && strings.Contains(ln, ": flags=") {
if curN != "" && isAct && (len(curIps) > 0 || curMac != "") {
interfaces = append(interfaces, NetworkInterface{Name: curN, MACAddress: curMac, IPAddresses: curIps, MTU: curMtu})
}
pts := strings.SplitN(ln, ":", 2)
curN = pts[0]
curIps = []string{}
curMac = ""
curMtu = ""
isAct = false
mtuM := mtuDRx.FindStringSubmatch(ln)
if len(mtuM) > 1 {
curMtu = mtuM[1]
}
}
Interface entries in ifconfig
output start with the interface name and flags line (not indented). When we encounter a new interface, we save the previous one if it was active and had either IP addresses or a MAC address. The MTU often appears on the same line as the interface name.
if curN == "" {
continue
}
if stActRx.MatchString(ln) {
isAct = true
}
mcM := mcDRx.FindStringSubmatch(ln)
if len(mcM) > 1 {
curMac = mcM[1]
}
ipM := ipDRx.FindStringSubmatch(ln)
if len(ipM) > 1 {
curIps = append(curIps, ipM[1])
}
ip6M := ip6DRx.FindStringSubmatch(ln)
if len(ip6M) > 2 {
curIps = append(curIps, ip6M[1]+"/"+ip6M[2])
}
}
if curN != "" && isAct && (len(curIps) > 0 || curMac != "") {
interfaces = append(interfaces, NetworkInterface{Name: curN, MACAddress: curMac, IPAddresses: curIps, MTU: curMtu})
}
}
return interfaces
For each line belonging to the current interface, we check for status information, MAC addresses, and IP addresses. IPv6 addresses get formatted with their prefix length (like “fe80::1⁄64”). We only include interfaces that are marked as active and have either IP addresses or MAC addresses, filtering out inactive or virtual interfaces that don’t provide useful network information.
The final check ensures we don’t miss the last interface in the output, since the loop only processes completed interfaces when it encounters a new one.
9. GPU Information
The getGPUInfo()
function extracts graphics card information using macOS’s system_profiler
command, which provides detailed hardware inventory data in a structured text format.
func getGPUInfo() []GPUInfo {
var gpus []GPUInfo
if runtime.GOOS == "darwin" {
out, e := runCommand("system_profiler", "SPDisplaysDataType")
if e != nil {
gpus = append(gpus, GPUInfo{Model: "N/A (system_profiler failed)"})
return gpus
}
The SPDisplaysDataType
argument tells system_profiler
to return only display/graphics information, which is much faster than generating a complete system report. If the command fails, we return a placeholder entry rather than an empty slice.
scan := bufio.NewScanner(strings.NewReader(out))
var curGPU *GPUInfo
chipRx := regexp.MustCompile(`Chipset Model:\s*(.*)`)
vendRx := regexp.MustCompile(`Vendor:\s*(.*)`)
vramTRx := regexp.MustCompile(`VRAM \(Total\):\s*(.*)`)
vramDRx := regexp.MustCompile(`VRAM \(Dynamic, Max\):\s*(.*)`)
The system_profiler
output uses a hierarchical format with key-value pairs. Each GPU appears as a separate section with various attributes. The VRAM patterns handle both discrete GPUs (which have dedicated VRAM) and integrated GPUs (which use shared system memory).
for scan.Scan() {
ln := strings.TrimSpace(scan.Text())
chipM := chipRx.FindStringSubmatch(ln)
if len(chipM) > 1 {
if curGPU != nil && curGPU.Model != "" {
gpus = append(gpus, *curGPU)
}
curGPU = &GPUInfo{Model: chipM[1]}
}
When we find a “Chipset Model” line, it indicates the start of a new GPU entry. We save the previous GPU (if it exists and has a model name) and create a new GPUInfo
struct. The chipset model becomes the GPU model name.
if curGPU == nil {
continue
}
vendM := vendRx.FindStringSubmatch(ln)
if len(vendM) > 1 {
curGPU.Vendor = vendM[1]
}
vramM := vramTRx.FindStringSubmatch(ln)
if len(vramM) > 1 {
curGPU.VRAM = vramM[1]
} else {
vramDM := vramDRx.FindStringSubmatch(ln)
if len(vramDM) > 1 && curGPU.VRAM == "" {
curGPU.VRAM = vramDM[1] + " (Shared/Dynamic)"
}
}
}
For subsequent lines, we look for vendor and VRAM information. Discrete GPUs typically show “VRAM (Total)” while integrated GPUs show “VRAM (Dynamic, Max)”. We prefer the total VRAM value and annotate dynamic VRAM to clarify that it’s shared system memory.
if curGPU != nil && curGPU.Model != "" {
if curGPU.Vendor != "" {
if reV := regexp.MustCompile(`(.*)\s*\(0x\w+\)`); reV.MatchString(curGPU.Vendor) {
curGPU.Vendor = strings.TrimSpace(reV.FindStringSubmatch(curGPU.Vendor)[1])
}
}
if curGPU.VRAM == "" {
curGPU.VRAM = "N/A (macOS)"
}
gpus = append(gpus, *curGPU)
}
if len(gpus) == 0 {
gpus = append(gpus, GPUInfo{Model: "N/A (No display found)"})
}
}
return gpus
After processing all lines, we handle the final GPU entry. Vendor names sometimes include hex identifiers in parentheses that we strip for cleaner output. If no VRAM information was found, we set it to “N/A (macOS)” to indicate this is a macOS-specific limitation rather than missing hardware.
The function ensures we always return at least one GPU entry, even if no display hardware is detected, which helps maintain consistent output formatting in the main program.
10. Concurrent Execution and Main Function
The main function orchestrates the hardware information gathering using Go’s concurrency features to collect data from multiple sources simultaneously, significantly reducing execution time.
func main() {
startTime := time.Now()
fmt.Println("Gathering system information...")
var wg sync.WaitGroup
var osInfo OSInfo
var cpuInfo CPUInfo
var memInfo MemoryInfo
var diskInfo []DiskInfo
var netInterfaces []NetworkInterface
var gpuInfo []GPUInfo
We declare variables to hold results from each hardware detection function and use sync.WaitGroup
to coordinate the concurrent execution. Recording the start time allows us to measure how much time the concurrent approach saves.
wg.Add(6)
go func() { defer wg.Done(); osInfo = getOSInfo() }()
go func() { defer wg.Done(); cpuInfo = getCPUInfo() }()
go func() { defer wg.Done(); memInfo = getMemoryInfo() }()
go func() { defer wg.Done(); diskInfo = getDiskInfo() }()
go func() { defer wg.Done(); netInterfaces = getNetworkInterfaces() }()
go func() { defer wg.Done(); gpuInfo = getGPUInfo() }()
wg.Wait()
Each hardware detection function runs in its own goroutine. The defer wg.Done()
ensures the WaitGroup counter decreases when each function completes, regardless of whether it succeeds or fails. This concurrent execution is safe because each function operates independently and writes to different variables.
fmt.Println("\n--- Operating System ---")
fmt.Printf("Name: %s\nVersion: %s\nKernel: %s\nHostname: %s\nArchitecture: %s\nGo Version: %s\nLogical CPUs: %d\n",
osInfo.Name, osInfo.Version, osInfo.Kernel, osInfo.Hostname, osInfo.Architecture, osInfo.GoVersion, osInfo.NumCPU)
fmt.Println("\n--- CPU ---")
fmt.Printf("Model: %s\nVendor: %s\nPhysical Cores: %d\nLogical Cores: %d\nFrequency: %s\nL2 Cache: %s\nL3 Cache: %s\n",
cpuInfo.Model, cpuInfo.VendorID, cpuInfo.Cores, cpuInfo.Threads, cpuInfo.Frequency, cpuInfo.L2Cache, cpuInfo.L3Cache)
The output formatting uses aligned labels for consistent presentation. Each section has a clear header and structured field display that makes the information easy to scan.
fmt.Println("\n--- Memory (RAM) ---")
fmt.Printf("Total: %s\nUsed: %s\nAvailable: %s\n", memInfo.TotalRAM, memInfo.UsedRAM, memInfo.AvailableRAM)
fmt.Println("--- Memory (Swap) ---")
fmt.Printf("Total: %s\nUsed: %s\nFree: %s\n", memInfo.TotalSwap, memInfo.UsedSwap, memInfo.FreeSwap)
fmt.Println("\n--- Disk Usage ---")
fmt.Printf("%-30s %-10s %-10s %-10s %-10s %s\n", "Filesystem", "Size", "Used", "Avail", "Use%", "Mounted on")
fmt.Println(strings.Repeat("-", 90))
for _, d := range diskInfo {
fs := d.FileSystem
if len(fs) > 28 {
fs = fs[:25] + "..."
}
if strings.HasPrefix(d.FileSystem, "map") && (d.Size == "0 B" || d.Size == "0") {
continue
}
fmt.Printf("%-30s %-10s %-10s %-10s %-10s %s\n", fs, d.Size, d.Used, d.Available, d.UsePercent, d.MountedOn)
}
The disk information uses tabular formatting with fixed-width columns. Long filesystem names get truncated with ellipses, and we filter out map-based filesystems with zero size (like some virtual filesystems) to keep the output focused on actual storage devices.
fmt.Println("\n--- Network Interfaces (Active) ---")
for _, iface := range netInterfaces {
fmt.Printf("Interface: %s\n", iface.Name)
if iface.MACAddress != "" {
fmt.Printf(" MAC Address: %s\n", iface.MACAddress)
}
if iface.MTU != "" {
fmt.Printf(" MTU: %s\n", iface.MTU)
}
if len(iface.IPAddresses) > 0 {
fmt.Printf(" IP Addresses: %s\n", strings.Join(iface.IPAddresses, ", "))
} else {
fmt.Printf(" IP Addresses: N/A\n")
}
}
fmt.Println("\n--- GPU(s) ---")
if len(gpuInfo) > 0 {
for i, gpu := range gpuInfo {
fmt.Printf("GPU %d:\n Vendor: %s\n Model: %s\n VRAM: %s\n", i, gpu.Vendor, gpu.Model, gpu.VRAM)
}
} else {
fmt.Println(" No GPU information found or an error occurred.")
}
fmt.Printf("\nReport generated in %s\n", time.Since(startTime))
Network interfaces and GPUs use hierarchical formatting with indented sub-fields. Multiple IP addresses get joined with commas, and GPU entries are numbered for systems with multiple graphics cards. The final timing information shows the performance benefit of concurrent execution - typically completing in under a second versus several seconds if run sequentially.
11. Final Implementation
The complete implementation brings together all the components we’ve built into a functional hardware detection tool. Here’s the full source code that demonstrates how each piece works together:To run this tool, save the code as main.go
and execute:
package main
import (
"bufio"
"fmt"
"os"
"os/exec"
"regexp"
"runtime"
"strconv"
"strings"
"sync"
"time"
)
type OSInfo struct {
Name string
Version string
Kernel string
Hostname string
Architecture string
GoVersion string
NumCPU int
}
type CPUInfo struct {
Model string
Cores int
Threads int
Frequency string
VendorID string
ModelName string
L2Cache string
L3Cache string
}
type MemoryInfo struct {
TotalRAM string
UsedRAM string
AvailableRAM string
TotalSwap string
UsedSwap string
FreeSwap string
}
type DiskInfo struct {
FileSystem string
Size string
Used string
Available string
UsePercent string
MountedOn string
}
type NetworkInterface struct {
Name string
MACAddress string
IPAddresses []string
MTU string
}
type GPUInfo struct {
Vendor string
Model string
VRAM string
}
func runCommand(name string, args ...string) (string, error) {
cmd := exec.Command(name, args...)
output, err := cmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf("command '%s %s' failed: %w\nOutput: %s", name, strings.Join(args, " "), err, string(output))
}
return strings.TrimSpace(string(output)), nil
}
func parseLine(line, key string) string {
if strings.HasPrefix(line, key) {
parts := strings.SplitN(line, ":", 2)
if len(parts) == 2 {
return strings.TrimSpace(parts[1])
}
}
return ""
}
func formatBytes(bytes uint64) string {
const unit = 1024
if bytes < unit {
return fmt.Sprintf("%d B", bytes)
}
div, exp := int64(unit), 0
for n := bytes / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %ciB", float64(bytes)/float64(div), "KMGTPE"[exp])
}
func isLikelyRawNumeric(s string) bool {
if s == "" {
return false
}
_, err := strconv.ParseUint(s, 10, 64)
return err == nil
}
func getOSInfo() OSInfo {
hostname, _ := os.Hostname()
kernel := "N/A"
osName := runtime.GOOS
osVersion := "N/A"
if runtime.GOOS == "linux" {
content, err := os.ReadFile("/etc/os-release")
if err == nil {
scanner := bufio.NewScanner(strings.NewReader(string(content)))
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "PRETTY_NAME=") {
osName = strings.Trim(strings.SplitN(line, "=", 2)[1], `"`)
} else if strings.HasPrefix(line, "VERSION=") && osVersion == "N/A" {
osVersion = strings.Trim(strings.SplitN(line, "=", 2)[1], `"`)
} else if strings.HasPrefix(line, "NAME=") && osName == runtime.GOOS {
osName = strings.Trim(strings.SplitN(line, "=", 2)[1], `"`)
}
}
}
out, err := runCommand("uname", "-r")
if err == nil {
kernel = out
}
if osVersion == "N/A" {
out, err = runCommand("lsb_release", "-ds")
if err == nil {
osName = strings.Trim(out, `"`)
} else {
out, err = runCommand("uname", "-v")
if err == nil {
osVersion = strings.Split(out, " ")[0]
}
}
}
} else if runtime.GOOS == "darwin" {
osNameFull, err := runCommand("sw_vers", "-productName")
if err == nil {
osName = osNameFull
}
osVer, err := runCommand("sw_vers", "-productVersion")
if err == nil {
osVersion = osVer
}
kern, err := runCommand("uname", "-r")
if err == nil {
kernel = kern
}
}
return OSInfo{Name: osName, Version: osVersion, Kernel: kernel, Hostname: hostname, Architecture: runtime.GOARCH, GoVersion: runtime.Version(), NumCPU: runtime.NumCPU()}
}
func getCPUInfo() CPUInfo {
info := CPUInfo{Threads: runtime.NumCPU()}
if runtime.GOOS == "linux" {
content, err := os.ReadFile("/proc/cpuinfo")
if err == nil {
scanner := bufio.NewScanner(strings.NewReader(string(content)))
var currentPhysicalID string
physicalCoresMap := make(map[string]bool)
for scanner.Scan() {
line := scanner.Text()
if val := parseLine(line, "vendor_id"); val != "" && info.VendorID == "" {
info.VendorID = val
}
if val := parseLine(line, "model name"); val != "" && info.ModelName == "" {
info.ModelName = val
}
if val := parseLine(line, "cpu MHz"); val != "" && info.Frequency == "" {
info.Frequency = val + " MHz"
}
if strings.HasPrefix(line, "physical id") {
if pVal := parseLine(line, "physical id"); pVal != "" {
currentPhysicalID = pVal
}
}
if strings.HasPrefix(line, "core id") {
if cVal := parseLine(line, "core id"); cVal != "" {
physicalCoresMap[currentPhysicalID+":"+cVal] = true
}
}
if val := parseLine(line, "cache size"); val != "" {
if info.L3Cache == "" {
info.L3Cache = val
} else if info.L2Cache == "" {
info.L2Cache = val
}
}
}
info.Cores = len(physicalCoresMap)
if info.Cores == 0 {
cpuCoresStr := ""
proc0content := ""
if len(string(content)) > 0 {
proc0content = strings.Split(string(content), "\n\n")[0]
}
scannerProc0 := bufio.NewScanner(strings.NewReader(proc0content))
for scannerProc0.Scan() {
if val := parseLine(scannerProc0.Text(), "cpu cores"); val != "" {
cpuCoresStr = val
break
}
}
if c, err := strconv.Atoi(cpuCoresStr); err == nil {
info.Cores = c
} else if info.Threads > 0 {
info.Cores = info.Threads / 2
if info.Cores == 0 {
info.Cores = 1
}
} else {
info.Cores = 1
}
}
}
lscpuOut, err := runCommand("lscpu")
if err == nil {
scanner := bufio.NewScanner(strings.NewReader(lscpuOut))
var coresPerSocketStr, socketsStr string
for scanner.Scan() {
line := scanner.Text()
if val := parseLine(line, "Model name"); val != "" {
info.ModelName = val
}
if val := parseLine(line, "CPU(s)"); val != "" {
if t, e := strconv.Atoi(val); e == nil {
info.Threads = t
}
}
if val := parseLine(line, "Core(s) per socket"); val != "" {
coresPerSocketStr = val
}
if val := parseLine(line, "Socket(s)"); val != "" {
socketsStr = val
}
if val := parseLine(line, "CPU max MHz"); val != "" {
info.Frequency = val + " MHz (Max)"
} else if val := parseLine(line, "CPU MHz"); val != "" && (info.Frequency == "" || !strings.Contains(info.Frequency, "Max")) {
info.Frequency = val + " MHz"
}
if val := parseLine(line, "Vendor ID"); val != "" {
info.VendorID = val
}
if val := parseLine(line, "L2 cache"); val != "" {
info.L2Cache = val
}
if val := parseLine(line, "L3 cache"); val != "" {
info.L3Cache = val
}
}
if cps, e1 := strconv.Atoi(coresPerSocketStr); e1 == nil {
if sks, e2 := strconv.Atoi(socketsStr); e2 == nil && cps > 0 && sks > 0 {
info.Cores = cps * sks
}
}
}
info.Model = info.ModelName
} else if runtime.GOOS == "darwin" {
model, _ := runCommand("sysctl", "-n", "machdep.cpu.brand_string")
info.Model = model
if c, e := runCommand("sysctl", "-n", "hw.physicalcpu"); e == nil {
if v, e2 := strconv.Atoi(c); e2 == nil {
info.Cores = v
}
}
if t, e := runCommand("sysctl", "-n", "hw.logicalcpu"); e == nil {
if v, e2 := strconv.Atoi(t); e2 == nil {
info.Threads = v
}
}
freqB, e := runCommand("sysctl", "-n", "hw.cpufrequency_max")
if e != nil || freqB == "" {
freqB, e = runCommand("sysctl", "-n", "hw.cpufrequency")
}
if e == nil {
if fq, e2 := strconv.ParseUint(freqB, 10, 64); e2 == nil {
info.Frequency = fmt.Sprintf("%.2f GHz", float64(fq)/1000000000.0)
}
}
if strings.Contains(info.Model, "Intel") {
info.VendorID = "GenuineIntel"
} else if strings.Contains(info.Model, "AMD") {
info.VendorID = "AuthenticAMD"
} else if strings.Contains(info.Model, "Apple") {
info.VendorID = "Apple"
}
l2B, e := runCommand("sysctl", "-n", "hw.l2cachesizepercore")
if e != nil || l2B == "" {
l2B, e = runCommand("sysctl", "-n", "hw.l2cachesize")
}
if e == nil {
if l2, e2 := strconv.ParseUint(l2B, 10, 64); e2 == nil {
info.L2Cache = formatBytes(l2)
}
}
l3B, e := runCommand("sysctl", "-n", "hw.l3cachesize")
if e == nil {
if l3, e2 := strconv.ParseUint(l3B, 10, 64); e2 == nil {
info.L3Cache = formatBytes(l3)
}
}
}
if info.Model == "" {
info.Model = "N/A"
}
if info.Cores == 0 && info.Threads > 0 {
info.Cores = info.Threads / 2
if info.Cores == 0 {
info.Cores = 1
}
}
if info.Threads == 0 && info.Cores > 0 {
info.Threads = info.Cores
}
if info.Threads == 0 && info.Cores == 0 {
info.Threads = 1
info.Cores = 1
}
if info.Frequency == "" {
info.Frequency = "N/A"
}
if info.L2Cache == "" {
info.L2Cache = "N/A"
}
if info.L3Cache == "" {
info.L3Cache = "N/A"
}
return info
}
func getMemoryInfo() MemoryInfo {
memInfo := MemoryInfo{}
if runtime.GOOS == "linux" {
content, err := os.ReadFile("/proc/meminfo")
if err == nil {
var tot, free, avail, buf, cach, swTot, swFree uint64
scan := bufio.NewScanner(strings.NewReader(string(content)))
for scan.Scan() {
ln := scan.Text()
flds := strings.Fields(ln)
if len(flds) < 2 {
continue
}
vS := flds[1]
v, _ := strconv.ParseUint(vS, 10, 64)
vB := v * 1024
switch flds[0] {
case "MemTotal:":
tot = vB
case "MemFree:":
free = vB
case "MemAvailable:":
avail = vB
case "Buffers:":
buf = vB
case "Cached:":
cach = vB
case "SwapTotal:":
swTot = vB
case "SwapFree:":
swFree = vB
}
}
memInfo.TotalRAM = formatBytes(tot)
if avail > 0 {
memInfo.AvailableRAM = formatBytes(avail)
memInfo.UsedRAM = formatBytes(tot - avail)
} else {
usd := tot - free - buf - cach
if usd > tot || usd < 0 {
usd = tot - free
}
memInfo.AvailableRAM = formatBytes(free + buf + cach)
memInfo.UsedRAM = formatBytes(usd)
}
memInfo.TotalSwap = formatBytes(swTot)
memInfo.FreeSwap = formatBytes(swFree)
if swTot > 0 {
memInfo.UsedSwap = formatBytes(swTot - swFree)
} else {
memInfo.UsedSwap = "0 B"
}
}
} else if runtime.GOOS == "darwin" {
var totMemB uint64
totMemS, e := runCommand("sysctl", "-n", "hw.memsize")
if e == nil {
if t, e2 := strconv.ParseUint(totMemS, 10, 64); e2 == nil {
totMemB = t
memInfo.TotalRAM = formatBytes(totMemB)
}
}
vmStatO, e := runCommand("vm_stat")
if e == nil && totMemB > 0 {
var pgSz uint64 = 16384
pgSzS, psE := runCommand("sysctl", "-n", "hw.pagesize")
if psE == nil {
if ps, ce := strconv.ParseUint(pgSzS, 10, 64); ce == nil {
pgSz = ps
}
}
var actC, inactC, wireC, comprC, freeC uint64
scan := bufio.NewScanner(strings.NewReader(vmStatO))
for scan.Scan() {
ln := scan.Text()
flds := strings.Fields(ln)
if len(flds) < 2 {
continue
}
vS := strings.TrimRight(flds[len(flds)-1], ".")
v, _ := strconv.ParseUint(vS, 10, 64)
if strings.HasPrefix(ln, "Page size:") && len(flds) >= 4 {
if ps, e2 := strconv.ParseUint(flds[3], 10, 64); e2 == nil {
pgSz = ps
}
}
switch {
case strings.HasPrefix(ln, "Pages active:"):
actC = v
case strings.HasPrefix(ln, "Pages inactive:"):
inactC = v
case strings.HasPrefix(ln, "Pages wired down:"):
wireC = v
case strings.HasPrefix(ln, "Pages occupied by compressor:"):
comprC = v
case strings.HasPrefix(ln, "Pages free:"):
freeC = v
}
}
usedB := (actC + wireC + comprC) * pgSz
memInfo.UsedRAM = formatBytes(usedB)
if totMemB >= usedB {
memInfo.AvailableRAM = formatBytes(totMemB - usedB)
} else {
memInfo.AvailableRAM = formatBytes((freeC + inactC) * pgSz)
}
} else {
memInfo.UsedRAM = "N/A"
memInfo.AvailableRAM = "N/A"
}
swUseO, e := runCommand("sysctl", "-n", "vm.swapusage")
if e == nil {
re := regexp.MustCompile(`total\s*=\s*([\d\.]+)(\w+)\s*used\s*=\s*([\d\.]+)(\w+)\s*free\s*=\s*([\d\.]+)(\w+)`)
m := re.FindStringSubmatch(swUseO)
if len(m) == 7 {
memInfo.TotalSwap = m[1] + m[2]
memInfo.UsedSwap = m[3] + m[4]
memInfo.FreeSwap = m[5] + m[6]
} else {
memInfo.TotalSwap = "N/A"
memInfo.UsedSwap = "N/A"
memInfo.FreeSwap = "N/A"
}
}
}
s := []string{memInfo.TotalRAM, memInfo.UsedRAM, memInfo.AvailableRAM, memInfo.TotalSwap, memInfo.UsedSwap, memInfo.FreeSwap}
for i, v_ := range s {
if v_ == "" {
s[i] = "N/A"
}
}
memInfo.TotalRAM, memInfo.UsedRAM, memInfo.AvailableRAM = s[0], s[1], s[2]
memInfo.TotalSwap, memInfo.UsedSwap, memInfo.FreeSwap = s[3], s[4], s[5]
return memInfo
}
func getDiskInfo() []DiskInfo {
var disks []DiskInfo
var out string
var err error
commandToUse := []string{"df", "-hP"}
humanReadableExpected := true
out, err = runCommand(commandToUse[0], commandToUse[1:]...)
if err != nil {
commandToUse = []string{"df", "-Pk"}
out, err = runCommand(commandToUse[0], commandToUse[1:]...)
if err != nil {
commandToUse = []string{"df"}
humanReadableExpected = false
out, err = runCommand(commandToUse[0], commandToUse[1:]...)
if err != nil {
return disks
}
}
}
lines := strings.Split(out, "\n")
if len(lines) > 1 {
for _, line := range lines[1:] {
fields := strings.Fields(line)
if len(fields) < 6 {
continue
}
disk := DiskInfo{
FileSystem: fields[0],
Size: fields[1],
Used: fields[2],
Available: fields[3],
UsePercent: fields[4],
MountedOn: strings.Join(fields[5:], " "),
}
needsConversion := !humanReadableExpected
if humanReadableExpected && isLikelyRawNumeric(disk.Size) {
needsConversion = true
}
if needsConversion {
var blockSizeMultiplier uint64 = 512
if commandToUse[0] == "df" && len(commandToUse) > 1 && commandToUse[1] == "-Pk" {
blockSizeMultiplier = 1024
}
if sizeVal, sErr := strconv.ParseUint(disk.Size, 10, 64); sErr == nil {
disk.Size = formatBytes(sizeVal * blockSizeMultiplier)
}
if usedVal, uErr := strconv.ParseUint(disk.Used, 10, 64); uErr == nil {
disk.Used = formatBytes(usedVal * blockSizeMultiplier)
}
if availVal, aErr := strconv.ParseUint(disk.Available, 10, 64); aErr == nil {
disk.Available = formatBytes(availVal * blockSizeMultiplier)
}
}
disks = append(disks, disk)
}
}
return disks
}
func getNetworkInterfaces() []NetworkInterface {
var interfaces []NetworkInterface
if runtime.GOOS == "linux" {
out, e := runCommand("ip", "addr")
if e != nil {
return interfaces
}
var curI *NetworkInterface
scan := bufio.NewScanner(strings.NewReader(out))
nmRx := regexp.MustCompile(`^\d+:\s+([\w.-]+):\s+<.*,UP.*>.*mtu\s+(\d+)`)
mcRx := regexp.MustCompile(`link/\w+\s+([0-9a-fA-F:]{17})`)
ipRx := regexp.MustCompile(`inet\s+([0-9\.]+/\d+)`)
ip6Rx := regexp.MustCompile(`inet6\s+([0-9a-fA-F:]+/\d+)`)
for scan.Scan() {
ln := strings.TrimSpace(scan.Text())
nmM := nmRx.FindStringSubmatch(ln)
if len(nmM) == 3 {
if curI != nil && (len(curI.IPAddresses) > 0 || curI.MACAddress != "") {
interfaces = append(interfaces, *curI)
}
curI = &NetworkInterface{Name: nmM[1], MTU: nmM[2]}
}
if curI != nil {
mcM := mcRx.FindStringSubmatch(ln)
if len(mcM) == 2 {
curI.MACAddress = mcM[1]
}
ipM := ipRx.FindStringSubmatch(ln)
if len(ipM) == 2 {
curI.IPAddresses = append(curI.IPAddresses, ipM[1])
}
ip6M := ip6Rx.FindStringSubmatch(ln)
if len(ip6M) == 2 {
curI.IPAddresses = append(curI.IPAddresses, ip6M[1])
}
}
}
if curI != nil && (len(curI.IPAddresses) > 0 || curI.MACAddress != "") {
interfaces = append(interfaces, *curI)
}
} else if runtime.GOOS == "darwin" {
out, e := runCommand("ifconfig", "-a")
if e != nil {
return interfaces
}
mcDRx := regexp.MustCompile(`ether\s+([0-9a-fA-F:]{17})`)
ipDRx := regexp.MustCompile(`inet\s+([0-9\.]+)\s+netmask`)
ip6DRx := regexp.MustCompile(`inet6\s+([0-9a-fA-F:]+)%?\w*\s+prefixlen\s+(\d+)`)
mtuDRx := regexp.MustCompile(`mtu\s+(\d+)`)
stActRx := regexp.MustCompile(`status:\s+active`)
var curN, curMtu, curMac string
var curIps []string
isAct := false
scan := bufio.NewScanner(strings.NewReader(out))
for scan.Scan() {
ln := scan.Text()
if !strings.HasPrefix(ln, "\t") && strings.Contains(ln, ": flags=") {
if curN != "" && isAct && (len(curIps) > 0 || curMac != "") {
interfaces = append(interfaces, NetworkInterface{Name: curN, MACAddress: curMac, IPAddresses: curIps, MTU: curMtu})
}
pts := strings.SplitN(ln, ":", 2)
curN = pts[0]
curIps = []string{}
curMac = ""
curMtu = ""
isAct = false
mtuM := mtuDRx.FindStringSubmatch(ln)
if len(mtuM) > 1 {
curMtu = mtuM[1]
}
}
if curN == "" {
continue
}
if stActRx.MatchString(ln) {
isAct = true
}
mcM := mcDRx.FindStringSubmatch(ln)
if len(mcM) > 1 {
curMac = mcM[1]
}
ipM := ipDRx.FindStringSubmatch(ln)
if len(ipM) > 1 {
curIps = append(curIps, ipM[1])
}
ip6M := ip6DRx.FindStringSubmatch(ln)
if len(ip6M) > 2 {
curIps = append(curIps, ip6M[1]+"/"+ip6M[2])
}
}
if curN != "" && isAct && (len(curIps) > 0 || curMac != "") {
interfaces = append(interfaces, NetworkInterface{Name: curN, MACAddress: curMac, IPAddresses: curIps, MTU: curMtu})
}
}
return interfaces
}
func getGPUInfo() []GPUInfo {
var gpus []GPUInfo
if runtime.GOOS == "linux" {
out, e := runCommand("lspci", "-nnk")
if e != nil {
out, e = runCommand("lspci")
if e != nil {
gpus = append(gpus, GPUInfo{Model: "N/A (lspci failed)"})
return gpus
}
}
scan := bufio.NewScanner(strings.NewReader(out))
var curGPU *GPUInfo
vgaRx := regexp.MustCompile(`VGA compatible controller|3D controller`)
devRx := regexp.MustCompile(`:\s*(.*)\s*\[(\w{4}:\w{4})\]`)
for scan.Scan() {
ln := scan.Text()
if vgaRx.MatchString(ln) {
if curGPU != nil {
if curGPU.VRAM == "" {
curGPU.VRAM = "N/A (Linux)"
}
gpus = append(gpus, *curGPU)
}
curGPU = &GPUInfo{VRAM: "N/A (Linux)"}
m := devRx.FindStringSubmatch(ln)
if len(m) > 1 {
fd := strings.TrimSpace(m[1])
pts := strings.SplitN(fd, ": ", 2)
if len(pts) == 2 {
curGPU.Vendor = strings.TrimSpace(pts[0])
curGPU.Model = strings.TrimSpace(pts[1])
} else {
curGPU.Model = fd
}
}
}
}
if curGPU != nil {
if curGPU.Model != "" && curGPU.VRAM == "" {
curGPU.VRAM = "N/A (Linux)"
}
if curGPU.Model != "" {
gpus = append(gpus, *curGPU)
}
}
if len(gpus) == 0 {
gpus = append(gpus, GPUInfo{Model: "N/A (No VGA/3D found)"})
}
} else if runtime.GOOS == "darwin" {
out, e := runCommand("system_profiler", "SPDisplaysDataType")
if e != nil {
gpus = append(gpus, GPUInfo{Model: "N/A (system_profiler failed)"})
return gpus
}
scan := bufio.NewScanner(strings.NewReader(out))
var curGPU *GPUInfo
chipRx := regexp.MustCompile(`Chipset Model:\s*(.*)`)
vendRx := regexp.MustCompile(`Vendor:\s*(.*)`)
vramTRx := regexp.MustCompile(`VRAM \(Total\):\s*(.*)`)
vramDRx := regexp.MustCompile(`VRAM \(Dynamic, Max\):\s*(.*)`)
for scan.Scan() {
ln := strings.TrimSpace(scan.Text())
chipM := chipRx.FindStringSubmatch(ln)
if len(chipM) > 1 {
if curGPU != nil && curGPU.Model != "" {
gpus = append(gpus, *curGPU)
}
curGPU = &GPUInfo{Model: chipM[1]}
}
if curGPU == nil {
continue
}
vendM := vendRx.FindStringSubmatch(ln)
if len(vendM) > 1 {
curGPU.Vendor = vendM[1]
}
vramM := vramTRx.FindStringSubmatch(ln)
if len(vramM) > 1 {
curGPU.VRAM = vramM[1]
} else {
vramDM := vramDRx.FindStringSubmatch(ln)
if len(vramDM) > 1 && curGPU.VRAM == "" {
curGPU.VRAM = vramDM[1] + " (Shared/Dynamic)"
}
}
}
if curGPU != nil && curGPU.Model != "" {
if curGPU.Vendor != "" {
if reV := regexp.MustCompile(`(.*)\s*\(0x\w+\)`); reV.MatchString(curGPU.Vendor) {
curGPU.Vendor = strings.TrimSpace(reV.FindStringSubmatch(curGPU.Vendor)[1])
}
}
if curGPU.VRAM == "" {
curGPU.VRAM = "N/A (macOS)"
}
gpus = append(gpus, *curGPU)
}
if len(gpus) == 0 {
gpus = append(gpus, GPUInfo{Model: "N/A (No display found)"})
}
}
return gpus
}
func main() {
startTime := time.Now()
fmt.Println("Gathering system information...")
var wg sync.WaitGroup
var osInfo OSInfo
var cpuInfo CPUInfo
var memInfo MemoryInfo
var diskInfo []DiskInfo
var netInterfaces []NetworkInterface
var gpuInfo []GPUInfo
wg.Add(6)
go func() { defer wg.Done(); osInfo = getOSInfo() }()
go func() { defer wg.Done(); cpuInfo = getCPUInfo() }()
go func() { defer wg.Done(); memInfo = getMemoryInfo() }()
go func() { defer wg.Done(); diskInfo = getDiskInfo() }()
go func() { defer wg.Done(); netInterfaces = getNetworkInterfaces() }()
go func() { defer wg.Done(); gpuInfo = getGPUInfo() }()
wg.Wait()
fmt.Println("\n--- Operating System ---")
fmt.Printf("Name: %s\nVersion: %s\nKernel: %s\nHostname: %s\nArchitecture: %s\nGo Version: %s\nLogical CPUs: %d\n",
osInfo.Name, osInfo.Version, osInfo.Kernel, osInfo.Hostname, osInfo.Architecture, osInfo.GoVersion, osInfo.NumCPU)
fmt.Println("\n--- CPU ---")
fmt.Printf("Model: %s\nVendor: %s\nPhysical Cores: %d\nLogical Cores: %d\nFrequency: %s\nL2 Cache: %s\nL3 Cache: %s\n",
cpuInfo.Model, cpuInfo.VendorID, cpuInfo.Cores, cpuInfo.Threads, cpuInfo.Frequency, cpuInfo.L2Cache, cpuInfo.L3Cache)
fmt.Println("\n--- Memory (RAM) ---")
fmt.Printf("Total: %s\nUsed: %s\nAvailable: %s\n", memInfo.TotalRAM, memInfo.UsedRAM, memInfo.AvailableRAM)
fmt.Println("--- Memory (Swap) ---")
fmt.Printf("Total: %s\nUsed: %s\nFree: %s\n", memInfo.TotalSwap, memInfo.UsedSwap, memInfo.FreeSwap)
fmt.Println("\n--- Disk Usage ---")
fmt.Printf("%-30s %-10s %-10s %-10s %-10s %s\n", "Filesystem", "Size", "Used", "Avail", "Use%", "Mounted on")
fmt.Println(strings.Repeat("-", 90))
for _, d := range diskInfo {
fs := d.FileSystem
if len(fs) > 28 {
fs = fs[:25] + "..."
}
if strings.HasPrefix(d.FileSystem, "map") && (d.Size == "0 B" || d.Size == "0") {
continue
}
fmt.Printf("%-30s %-10s %-10s %-10s %-10s %s\n", fs, d.Size, d.Used, d.Available, d.UsePercent, d.MountedOn)
}
fmt.Println("\n--- Network Interfaces (Active) ---")
for _, iface := range netInterfaces {
fmt.Printf("Interface: %s\n", iface.Name)
if iface.MACAddress != "" {
fmt.Printf(" MAC Address: %s\n", iface.MACAddress)
}
if iface.MTU != "" {
fmt.Printf(" MTU: %s\n", iface.MTU)
}
if len(iface.IPAddresses) > 0 {
fmt.Printf(" IP Addresses: %s\n", strings.Join(iface.IPAddresses, ", "))
} else {
fmt.Printf(" IP Addresses: N/A\n")
}
}
fmt.Println("\n--- GPU(s) ---")
if len(gpuInfo) > 0 {
for i, gpu := range gpuInfo {
fmt.Printf("GPU %d:\n Vendor: %s\n Model: %s\n VRAM: %s\n", i, gpu.Vendor, gpu.Model, gpu.VRAM)
}
} else {
fmt.Println(" No GPU information found or an error occurred.")
}
fmt.Printf("\nReport generated in %s\n", time.Since(startTime))
}
Run commands:
go run main.go
Or build and run as a binary:
go build -o hwinfo main.go
./hwinfo
The tool provides comprehensive hardware information including CPU details, memory usage, disk space, active network interfaces, and GPU specifications. The concurrent execution typically completes in under a second, making it efficient for scripting or monitoring applications.
This implementation demonstrates practical system programming in Go, showing how to execute system commands, parse their output, handle errors gracefully, and use concurrency effectively. The code is structured to be easily extensible - you can add new hardware detection functions by following the same pattern of system command execution and output parsing.
$ ./hwinfo
Gathering system information...
--- Operating System ---
Name: macOS
Version: 13.2.1
Kernel: 22.3.0
Hostname: MacBook-Pro.local
Architecture: arm64
Go Version: go1.21.0
Logical CPUs: 10
--- CPU ---
Model: Apple M1 Pro
Vendor: Apple
Physical Cores: 10
Logical Cores: 10
Frequency: N/A
L2 Cache: 12.0 MiB
L3 Cache: N/A
--- Memory (RAM) ---
Total: 16.0 GiB
Used: 8.2 GiB
Available: 7.8 GiB
--- Memory (Swap) ---
Total: 1.0G
Used: 512.00M
Free: 512.00M
--- Disk Usage ---
Filesystem Size Used Avail Use% Mounted on
------------------------------------------------------------------------------------------
/dev/disk3s1s1 460.4 GiB 15.2 GiB 178.4 GiB 8% /
/dev/disk3s6 460.4 GiB 1.2 GiB 178.4 GiB 1% /System/Volumes/VM
/dev/disk3s2 460.4 GiB 384.9 MiB 178.4 GiB 1% /System/Volumes/Preboot
/dev/disk3s4 460.4 GiB 14.9 MiB 178.4 GiB 1% /System/Volumes/Update
/dev/disk1s2 500.1 MiB 6.1 MiB 481.5 MiB 2% /System/Volumes/xART
/dev/disk1s1 500.1 MiB 7.4 MiB 481.5 MiB 2% /System/Volumes/iSCPreboot
/dev/disk1s3 500.1 MiB 1.2 MiB 481.5 MiB 1% /System/Volumes/Hardware
--- Network Interfaces (Active) ---
Interface: en0
MAC Address: a4:83:e7:2a:5f:c8
MTU: 1500
IP Addresses: 192.168.1.145, fe80::1c85:2ff4:a8e3:d127/64
Interface: awdl0
MAC Address: b2:f4:29:8d:3a:e1
MTU: 1500
IP Addresses: fe80::b0f4:29ff:fe8d:3ae1/64
Interface: llw0
MAC Address: b2:f4:29:8d:3a:e1
MTU: 1500
IP Addresses: fe80::b0f4:29ff:fe8d:3ae1/64
Interface: utun0
MTU: 1380
IP Addresses: fe80::a4c3:d9ff:fe7b:1234/64
--- GPU(s) ---
GPU 0:
Vendor: Apple
Model: Apple M1 Pro
VRAM: N/A (macOS)
Report generated in 487ms
GitHub Repository: You can find the complete source code at: https://github.com/rezmoss/macos-hardware-detection-go