yes

Instead of "Hello, World!", we rebelliously open with a real utility, a basic version of the yes tool. In C:

#include <stdio.h>
int main() { for(;;) puts("y"); }

A direct Go translation trades parentheses and semicolons for braces:

package main
import "fmt"
func main() { for { fmt.Println("y") } }

We deviate from standard style for compactness. Run gofmt to fix this.

Yes, really

When given arguments, instead of y, the yes program is supposed to print all the arguments separated by single spaces, or as we might say in C:

#include <stdio.h>
int main(int argc, char **argv) {
  for(;;) {
    if (argc < 2) puts("y"); else {
      for(int i = 1; i < argc; i++) {
        if (i > 1) putchar(' ');
        printf("%s", argv[i]);
      }
      putchar('\n');
    }
  }
}

Again, Go is marginally lengthier:

package main
import ("flag";"fmt")
func main() {
  flag.Parse()
  for {
    if flag.NArg() == 0 { fmt.Println("y") } else {
      for i:=0; i<flag.NArg(); i++ {
        if i>0 { fmt.Print(" ") }
        fmt.Print(flag.Arg(i))
      }
      fmt.Println()
    }
  }
}

To compensate, Go is arguably clearer. C veterans instantly recognize argc and argv, but these are inscrutable compared to Go’s flag package. Also, C counts the program’s name as the first command-line argument, which can cause off-by-one confusion.

There are only 3 semicolons, and even they could be replaced with newlines. This reduces clutter, though you can always append semicolons to statements if you miss them!

Like JavaScript, Go inserts semicolons automatically. Unlike JavaScript, Go’s scheme is simple and useful. However, care is needed. As a rule, never start a new line with an opening brace; it belongs with the previous line. Go style is akin to The One True Brace Style of C.

Error-handling

When writes fail, our program should print an error message and exit.

We can reduce the number of annoying error checks by consolidating the write statements: we fill a buffer with the command-line arguments, correctly formatted, and then we need only look for errors when dumping this buffer. This design may be preferable in any case, as it moves most of the processing out of the loop.

Because the arguments' lengths are initially unknown, in C, we must resort to heap allocation:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char **argv) {
  char *s = 0;
  int n = 0;
  if (argc < 2) s = "y\n", n = 2; else {
    for(int i = 1; i < argc; i++) n += strlen(argv[i]) + 1;
    if (!(s = malloc(n+1))) perror("yes"), exit(1);
    strcpy(s, argv[1]);
    for(int i = 2; i < argc; i++) strcat(strcat(s, " "), argv[i]);
    strcat(s, "\n");
  }
  for(;;) if (n != fwrite(s, 1, n, stdout)) perror("yes"), exit(1);
}

We need to allocate one extra byte because sprintf() writes a trailing zero byte.

With Go, we can do without fiddly memory calculations, and its string built-in type further reduces verbosity:

package main
import ("flag";"os";"strings")
func main() {
  flag.Parse()
  s := "y\n"
  if flag.NArg() > 0 {
    s = strings.Join(flag.Args(), " ") + "\n"
  }
  for {
    if _,er := os.Stdout.WriteString(s); er != nil {
      println("yes:", er)
      os.Exit(1)
    }
  }
}

Go is simpler and hence clearer. Error handling is particularly elegant in Go thanks to multiple return values. Returning errors in C is unpleasant: as seen here, getchar() returns an int, not a char, so it can use the special EOF value to indicate an error. Other functions might return -1, or NULL, or use some other ad hoc scheme.

Sometimes, special error return values are convenient. But when they aren’t, what can we do? If we insist on out-of-band errors in a C function that already returns a value, we must either define a struct to hold a value/error tuple, or add pointer arguments.

Multiple return values neatly solve our dilemma. They also simplify swapping two variables:

a, b = b, a

Incidentally, we’ve switched from Printf to WriteString because we have no need for formatting: one fewer package to import.

Strings, memory

Strings are immutable in Go, so the above code allocates memory for every concatenation. If this mattered, we could work with a buffer in-place with the bytes package:

...
  if flag.NArg() > 0 {
    b := bytes.NewBufferString("")
    for i := 0; i < flag.NArg(); i++ {
      if i > 0 { b.WriteByte(' ') }
      b.WriteString(flag.Arg(i))
    }
    b.WriteByte('\n')
    s = b.String()
  }
...

An unsuccessful malloc has no analog in Go. On many systems, this hardly differs from C due to overcommitting: the kernel returns a valid pointer for a memory allocation request even when there is insufficient memory to honour the request, and runs into trouble when a program tries to use its memory. Thus a C malloc() may not return NULL even if we are out of memory.





Ben Lynn blynn@cs.stanford.edu