Hacker News new | ask | show | jobs
by sephware 2756 days ago
Without taking sides on the issue, these are purposeful trade-offs that the Go team makes. They are trying to avoid bloating the language and making it too difficult for them to add optimizations or other features. And I think they would argue that it makes ignoring errors harder, since you have to intentionally discard the error value when it was given to you, which takes conscious effort, rather than omitting it from the code completely like other languages let you.
3 comments

> these are purposeful trade-offs that the Go team makes.

Trade-offs for whom or with what in mind? the compiler or the programmer?

That's the "philosophical" difference between the Rust team and the Go team.

The cynic in me says it's optimizing to hire lots of low-skill programmers at Google for cheap.
Exactly. That is why Rust does so many more things, but Go compiles so fast.

Even with two wildly different approaches to language design, each has their place, and their own set of drawbacks.

It's a false dichotomy. Languages like Java or Kotlin compile fast and have exceptions too.
Because the JVM takes another tradeoff against Go and Rust: it has a runtime interpreter+JIT, which causes even more performance overhead than Go's GC let alone Rust which - at runtime - is effectively a zero-cost abstraction over C and jemalloc.
The JVM GCs generally have much lower overhead than Go's does. Go imposes enormous collection costs on apps to try and drive latency so low. This is widely acknowledged in discussions of the various GC tradeoffs.
The GCs alone? Yes. The rest of their respective runtime architectures? Highly doubtful. As GC-dependent as Go may be, it still does end up as native code.

But frankly, comparing Go to Java when things like Rust exist ... seems analogous to comparing the z80 to the AVR when 32-bit ARM cores exist.

Not quite - Java is faster than Kotlin [0] but I've never seen a Golang app take more than 10 seconds (and that's something huge like Kubernetes or Docker components)

[0] https://medium.com/keepsafe-engineering/kotlin-vs-java-compi...

The article you link comes to a different conclusion:

> With the Gradle daemon running and incremental compilation turned on, Kotlin compiles as fast or slightly faster than Java.

Go optimizes for fast compilation, as such the language has fewer internal checks for only the absolutely necessary. In my opinion, to be able to continue execution when an error happened earlier could be a blessing or a curse, and it depends on the programmer. I've been writing Go for 2 years now and I don't have a single case where I accidentally ignored an error. I have ignored errors, but usually for operations whose end result I don't care about or doesn't affect what happens next in the program.

There's also a recommended way of dealing with errors. I may not state it correctly but basically you build a pipeline of operations for a data type that represents the arguments (or data) of the operations. For example, to compress an image given a URL:

  type Image struct {
    src string
    bytes []byte
    width uint
    height uint
    err error
  }
  
  func NewImage(src string) Image {
    return Image{src: src}
  }

  func (img Image) Get() {
    if err != nil {
      return
    }
    ...
    // Set error value if this operation fails
  }
  
  func (img Image) Compress {
    if err != nil {
      return
    }
    ...
    // Set error value if this operation fails
  }
  
  func (img Image) Err() error { return img.err }
  
  img := NewImage("https://image.src/random-image.png")
  img.Get()
  img.Compress()
  if img.Err() != nil {
    // handle error just once
  }
this does work... but it also requires two very significant points.

1: you must do this wrapping yourself, for everything you wish to simplify, as few libs do it.

2: you now have non-standard error handling and your tools will not warn you if you handle it wrong.

the second one, to me, is borderline fatal for this pattern. You can't look at this code and realize it's missing error handling (or doing it incorrectly), and you can't run `errcheck` to tell you that e.g. you're missing `err := img.Get()`.

And all the code to satisfy borrow checker in Rust, is it for compiler or programmer?
That’s like asking “are type signatures for the compiler, or the programmer?” The answer is a bit of both: for the programmer to express intent, and for the compiler to check that intent is correct.
Why would a programmer have an intent about types to express unless s/he had been trained to think of programming in terms of static typing?
If your type system is powerful enough (like in Rust), then you can use it to express relations in your problem domain and have compiler enforce them for you before the code even runs.

Example: https://blog.chain.com/bulletproof-multi-party-computation-i...

I think you missed the "unless" in my question. I am well aware that having chosen to treat types as a first class problem, a coder can leverage that intent. My point is simply that there is a populous, productive world of software development in which typing is a humdrum issue for the coders. C.f. the last 30 years of Javascript and Python
What makes that an interesting question? That’s like saying “how would someone have an idea to tests their code unless they were trained to think about testing?” It feels like a tautology, and not particularly relevant.

(I love both dynamic and static typing. I think dynamic type systems are more useful than basic static type systems, but also really enjoy more expressive static type systems.)

Perhaps the programmer should be focused on the purpose the code serves, rather than the code as code object. Mental cycles devoted to thinking about types are, for most programs, overhead. It's perfectly reasonable to be a strong static-typing believer, though I am not. But it's also perfectly reasonable not to think about types when coding, as millions of useful programs have demonstrated.
Typing doesn't have to be static (or strong) to be there. When I write `a + b`, I intend `a` and `b` to be things such that the `+` operator makes sense for them. Might be two numbers, might be two strings, might be a string and a number if I'm using js/php, the point is that I always have an intention, implicit or explicit.
"When I walk to the store, I intend for the ground to hold me up. My intention might be implicit, but it is always there."
If it would bring significant amount of technical debt to introduce some synctactic sugar that'd allow to bubble errors up the call stack without 3 extra lines of code, then design choices made for Go seem to be questionable. At least superficially.
I find this perspective curious. Maybe it's the non-web field that I work in, but I've never written a program where I could unintentionally ignore errors and have things go as expected after.

What are some cases where errors can be ignored? And if they can be, are these neccesarily some sort of "status" rather than "error"?

Well look at: https://golang.org/pkg/database/sql/#DB.Query

How likely is the error going to happen, if you know (through tests and everything) that the query is correct?

it can only happen in two scenarios:

1. the database has serious problems 2. the driver is incorrect

in normal cases you would probably handle that with a middleware in classical languages. i.e. you would have a middlewre that catches exceptions and handle the error there (logging, paging, whatever) and maybe show a nice looking "we are experience problems now"-page.

in golang this is a little bit harder, since you would need to handle the error by every caller and with the default http interface you would actually need to call your "handle default error" in every http handler.

If you're writing a command line utility that tries to load a config file and falls back on defaults if it's not found, you may try to load it and just ignore any error, only caring that it either returns a path if found or null if you should use default values.
I believe what you are saying is that you can ignore errors if you check or rely on the side effects of an error (such as the result of a function being null on error).

One could argue that the side effect is part of the error, and really what you are ignoring are the details of an error (e.g. did the config entry fail to load because the config file wasn't found, because of filesystem permissions, or because the particular configuration key wasn't present?)

That's a good way to put it, and yeah I would agree that's how I like to use errors: give me the details I ask for and let me ignore the ones I don't care about. Sometimes an "error" isn't really an error, and at those times exceptions are overkill. Sometimes an error really is an error and I need to let the user know something went wrong, give them enough details to let them fix it and wait for them to try again.
But wouldn't that look like:

   var s Settings
   if s, err := load_settings(); err != nil {
      s = defaults()
   }
Where is the unhandled error?
Note that your code will not work; you've shadowed 's' inside the if block because you have 'if s, err := ...'. As a result, 's = defaults()' will set the shadowed 's' variable that only exists within the if block to defaults, but the 'var s Settings' you declared above will not be modified.

Here's a playground showing the issue: https://play.golang.org/p/eE0HEZx1MJu

Go is full of nice little foot-guns like that :)

You would want to also have 'var err error' above the block and use '=' instead of ':=' to fix this sort of issue.

I am by no means an expert (or even a frequent user) of go. However, the most unfortunate foot cannon I have encountered is this:

    package main
    
    type HowOdd interface {
    	Poke() string
    }
    
    type AConcreteThing struct {
    }
    
    func (t *AConcreteThing) Poke() string { return "hehe" }
    
    func DoAConcreteThing() *AConcreteThing {
    	return nil
    }
    
    func DoAThing() HowOdd {
    	return DoAConcreteThing()
    }
    
    func main() {
    	lolWhat := DoAThing()
    
    	if lolWhat != nil {
    		panic("How could this happen?")
    	}
    }
    
I understand that the interface at the bottom is a non-null interface containing a null value, but...damn, that sucks.
An example is logging something to a remote server for analytics purposes: you can fire and forget.