Recently run into this when developing my helper package.
Base on example code here: https://go-review.googlesource.com/c/go/+/213337/1/src/os/exec/example_test.go
func ExampleExitError() {
    cmd := exec.Command("sleep", "-u")
    err := cmd.Run()
    var exerr *exec.ExitError
    if errors.As(err, &exerr) {
        fmt.Printf("the command exited unsuccessfully: %d\n", exerr.ExitCode())
}
The Exit Code
I end up doing the following for my own exec cmd wrapper:
// Return exit code
func (self *MyCmd) ExitCode() int {
    var exitErr *exec.ExitError
    if errors.As(self.Err, &exitErr) {
        return exitErr.ExitCode()
    }
    // No error
    return 0
}
self.Err is the return value from a exec.Command.Run(). Complete listing is here: https://github.com/J-Siu/go-helper/blob/master/myCmd.go
Text Error Message
While @colm.anseo answer take consideration of os.patherror, it doesn't give an error code(int), and IMHO should be handled separately. Instead text error message can be extract from execCmd.Stderr like follow:
func (self *MyCmd) Run() error {
    execCmd := exec.Command(self.CmdName, *self.ArgsP...)
    execCmd.Stdout = &self.Stdout
    execCmd.Stderr = &self.Stderr
    execCmd.Dir = self.WorkDir
    self.CmdLn = execCmd.String()
    self.Err = execCmd.Run()
    self.Ran = true
    ReportDebug(&self, "myCmd:", false, false)
    ReportDebug(self.Stderr.String(), "myCmd:Stderr", false, false)
    ReportDebug(self.Stdout.String(), "myCmd:Stdout", false, false)
    return self.Err
}
self.Stderr is a bytes.Buffer and pass into execCmd before Run(). After execCmd.Run(), text err can be extracted with self.Stderr.String().