Go error handling is even worse than sh [Sunday, 2012-10-28]

While writing zwm I started using xgb code to understand how the protocol works, because the specs are practically useless.

The first thing I did with it was writing the code to read the authority files to get the name and the data for the authentication process.

This is the code it uses to get that data:

// readAuthority reads the X authority file for the DISPLAY.
// If hostname == "" or hostname == "localhost",
// then use the system's hostname (as returned by os.Hostname) instead.
func readAuthority(hostname, display string) (
  name string, data []byte, err error) {

  // b is a scratch buffer to use and should be at least 256 bytes long
  // (i.e. it should be able to hold a hostname).
  b := make([]byte, 256)

  // As per /usr/include/X11/Xauth.h.
  const familyLocal = 256

  if len(hostname) == 0 || hostname == "localhost" {
    hostname, err = os.Hostname()
    if err != nil {
      return "", nil, err
    }
  }

  fname := os.Getenv("XAUTHORITY")
  if len(fname) == 0 {
    home := os.Getenv("HOME")
    if len(home) == 0 {
      err = errors.New("Xauthority not found: $XAUTHORITY, $HOME not set")
      return "", nil, err
    }
    fname = home + "/.Xauthority"
  }

  r, err := os.Open(fname)
  if err != nil {
    return "", nil, err
  }
  defer r.Close()

  for {
    var family uint16
    if err := binary.Read(r, binary.BigEndian, &family); err != nil {
      return "", nil, err
    }

    addr, err := getString(r, b)
    if err != nil {
      return "", nil, err
    }

    disp, err := getString(r, b)
    if err != nil {
      return "", nil, err
    }

    name0, err := getString(r, b)
    if err != nil {
      return "", nil, err
    }

    data0, err := getBytes(r, b)
    if err != nil {
      return "", nil, err
    }

    if family == familyLocal && addr == hostname && disp == display {
      return name0, data0, nil
    }
  }
  panic("unreachable")
}

func getBytes(r io.Reader, b []byte) ([]byte, error) {
  var n uint16
  if err := binary.Read(r, binary.BigEndian, &n); err != nil {
    return nil, err
  } else if n > uint16(len(b)) {
    return nil, errors.New("bytes too long for buffer")
  }

  if _, err := io.ReadFull(r, b[0:n]); err != nil {
    return nil, err
  }
  return b[0:n], nil
}

func getString(r io.Reader, b []byte) (string, error) {
  b, err := getBytes(r, b)
  if err != nil {
    return "", err
  }
  return string(b), nil
}

This is my translation to Zsh:

function X:auth:read-short {
	integer fd="$2"
	local encoded

	sysread -i $fd -s 2 encoded || return $?

	local first="$encoded[1]"
	local second="$encoded[2]"

	typeset -g "${1:-REPLY}=$(( #first << 8 | #second ))"
}

function X:auth:read-string {
	integer fd="$2"
	integer length

	X:auth:read-short length "$fd"
	sysread -i $fd -s $length $1 || return $?
}

function X:auth:read {
	local display="$2"
	local hostname="${3:-$HOST}"
	local authority="${XAUTHORITY:-$HOME/.Xauthority}"
	integer fd

	exec {fd}<"$authority"

	if [[ -z $fd ]]; then
		return 1
	fi

	integer family
	local addr
	local disp
	local name
	local data

	while true; do
		X:auth:read-short  family $fd || break
		X:auth:read-string addr   $fd || break
		X:auth:read-string disp   $fd || break
		X:auth:read-string name   $fd || break
		X:auth:read-string data   $fd || break

		if [[ "$family" == 256 && "$addr" == "$hostname" && "$disp" == "$display" ]]
    then
			set -A "${1:-reply}" name "$name" data "$data"

			break
		fi
	done

	exec {fd}>&-

	(( ${#${(P)1:-reply}} ))
}

Error handling is easier and better looking in shell scripts than with Go, most of the code in the loop in the Go code is error checking and returning the same thing.

Now that could have been easily rewritten like I did my thing, but the way he did it is idiomatic Go, with exceptions that would have been way easier on the eyes and easier to understand too.

But who am I to talk badly about Go? I’m the one writing a window manager in Zsh.