A few proposals for Go2


(Kerokero) #1

I’m an experienced pythonista. Lately I’ve been trying out Go by translating some of my small programs into it.

So far I like it, but I found some rough edges; some about ergonomics, some about not protecting from accidental errors.

As a begginer, I might be missing the right way to do it, but here are my thoughts so far.

(Is this the correct forum to discuss proposals?)

defer in loop should be a compiler error

This compiles, but leads to nasty bugs:

files, err := ioutil.ReadDir("./")
if err != nil {
  log.Fatal(err)
}

for _, f := range files {
  name := path.Join("./", f.Name())
  handle, err := os.Open(name)
  defer handle.Close()
  if err != nil {
    continue
  }

  // just for the sake of example
  if true {
    continue
  }

Backwards-incompatible, but probably there are no situations where it’s legit to use.

It could be just a warning to ease migration.

error checking should always be enforced

This compiles:

handle.Seek(4, 0)

It should be:

_, _ = handle.Seek(4, 0)

Backwards-compatible, gofix-able.

error handling: try operator

Compare this current code:

func main() {
	fileName := "a file"
	_, err := read(fileName)
	if err != nil {
		log.Fatal(err)
	}
}

func read(fileName string) (uint32, error) {
	handle, err := os.Open(fileName)
	defer handle.Close()
	if err != nil {
		return 0, err
	}

	if _, err := handle.Seek(-4, 2); err != nil {
		return 0, err
	}

	bs := make([]byte, 4)
	_, err = handle.Read(bs)
	if err != nil {
		return 0, err
	}
	offset := binary.BigEndian.Uint32(bs)

	if _, err = handle.Seek(int64(offset), 0); err != nil {
		return 0, err
	}

	if _, err = handle.Seek(4, 1); err != nil {
		return 0, err
	}

	bs = make([]byte, 4)
	_, err = handle.Read(bs)
	if err != nil {
		return 0, err
	}

	return binary.BigEndian.Uint32(bs), nil
}

… to this proposal inspired by Rust:

func main() {
	fileName := "a file"
	_, err := read(fileName)
	if err != nil {
		log.Fatal(err)
	}
}

func read(fileName string) (uint32, error) {
	handle, _ := os.Open(fileName)?
	defer handle.Close()

	handle.Seek(-4, 2)?

	offset := binary.BigEndian.Uint32(handle.Read(make([]byte, 4))?)

	handle.Seek(int64(offset), 0)?

	handle.Seek(4, 1)?

	return binary.BigEndian.Uint32(handle.Read(make([]byte, 4))?), nil
}

The code is far more readable, but still backwards-compatible.

functions: named args and default values

Compare this current code:

if _, err := os.Stat(path); err != nil {
  if os.IsNotExist(err) {
    os.Mkdir(path, 0777)
  } else {
    log.Fatal(err)
  }
}

With this proposal:

if _, err := os.Mkdir(path, existOk=true); err != nil {
  log.Fatal(err)
}

Or better, with also my previous proposal:

os.Mkdir(path, existOk=true)?

Not backwards-compatible (?).


(George Calianu) #2

Well, from the beginning Go has some criticism about the lack of features founded in other languages. The authors of the language often explained why Go is a simple language and why is important to remain simple. Rob Pike has some talks about the principles of simplicity (see below one of them). Rob, also speeks about a kind of languages convergence, where the languages, in time, borrow features from one to another and become more and more complex and that is what Go tried to avoid. On short, adding features won’t make Go better.


(Kerokero) #3

I understand. What about the first two points (having compiler warnings for defer in loop and unused error value)? They are more about preventing bad use of current features that leads to bug, rather than adding new features.


(George Calianu) #4

Maybe can be useful in your particular case but surely defer need to be used repeatedly, no matter it’s in a loop or not (see this playground). Of course bad coding can lead to bugs but this is a common case the programing.

_ is not mandatary but useful to discard some values as you need, when you have situations like that

i, _ = handle.Seek(4, 0)

or

_, err = handle.Seek(4, 0)

Also other combinations are possible. In my opinion it cannot be limited or forced to an imperative construction.


(Kerokero) #5

good points, thank’you for explaining


(Glenn Hancock) #6

Another thought about your second point of forcing a fatal error if err is no null. There are plenty of cases that you wouldn’t want your service to crash completely due to an error but you do want to log the error for review later. ie: a bad database connection might send back a try again message but I don’t want to take the service away from everyone else just because I had a minor hiccup.

Just my 2 cents,


(Kerokero) #7

I didn’t mean it like that (log.Fatal was just an example). The idea was disallowing a bare “handle.Seek(4, 0)”, which hides the fact the Seek has return values, and instead enforcing using “_, _ = handle.Seek(4, 0)” if you don’t care about catching the error.