Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

This isn’t true. Go has a basic standard package list. https://pkg.go.dev/std


Another big area that is lacking is Go makes it so hard if you want to use something other than a primitive as a hash key (Rust is guilty of this as well, mind you). This is something that should come out of the box in any modern language in my opinion.


> Rust is guilty of this as well, mind you

#[derive(Hash)] struct X{ ... }

Seems easy enough to me? The only annoyance is if a third party type didn't implement Hash, but you can solve that with a manual implementation instead of a derive.


> you can solve that with a manual implementation instead of a derive.

How? I thought the orphan rule said you can only define trait implantations at struct definition or trait definition


Yeah, for foreign types you will have to use the "newtype" pattern, aka: define your own wrapper struct.


ding ding ding! This has been a problem for me a number of times. The quick workaround is pray that it implements Debug and in a reasonable way and take the hash of the string instead


Maybe I misunderstood you, but you can use structs as keys of a map.


not if it doesn't have equailty defined. Then you have to define it. Oh, and it's an AST node someone at X company wrote and there are 100+ node types. Have fun!

In crystal there is a reasonable data-based default hash implementation that you can just rely on.


The map key type can be anything for which `==` is defined. This includes structs for which `==` is defined for every field. Composite keys etc are trivial.

https://go.dev/ref/spec#Map_types


Yes, and it's missing tons of things we take for granted in other langauges. If you look at the "strings" section it's so short. There aren't even convenience things like string reverse.


Odd, I find the standard library very full-fledged and useful, including the strings package. I've almost never needed to reverse a string, except in interviews. :-) The stdlib has a full (and good) HTTP server with HTTP/2 and TLS support, HTML templating, excellent I/O support, compression, even image encoding/decoding and drawing. That said, the container types are pretty sparse, but that may change a bit now that generics are here.

What, apart from string reverse, do you miss for real projects?


The fact that it's a bunch of top level functions also makes me think of PHP from the early 00s, only I was able to do more with things then tbh. Just feels super archaic.


Not that person, but I'll call out a few issues I've had with the go stdlib:

First, it has a lot of useless packages you typically wouldn't use, like "log" and "flag" (which work, but are way worse than third party alternatives like logrus and pflag), but also like "syscall" (as it says 'deprecated, use 'golang.org/x/sys' instead), "image/draw" (nope, you wanted 'golang.org/x/image/draw' usually), "path" for working with paths (you wanted "filepath"), "net/rpc" and "rpc/jsonrpc", "plugin" (almost always a bad idea), a chunk of "strings" (use "golang.org/x/text" for proper unicode support) and so on. Some of those are marked deprecated, most of them are not, and are just waiting for someone to accidentally use them.

That's issues I have with the stdlib and not stuff I'm missing though... Though I guess I really do miss a good logging library, or at least interface for external packages to implement so I can plug in logging libraries without rewriting the world.

One thing I do find missing frequently is a reasonable set type with the ability to do things like basic set operations (intersect, diff, etc). I constantly have to write ad-hoc for loops in go to do set operations, and it's verbose, non-obvious what the code does, and easy to get wrong.

But honestly, the main thing I'm missing isn't actually a package, but more about error handling for the stdlib as a whole, which is more a language issue. I really wish I could know what possible errors stdlib functions returned without, fairly often, having to read huge chunks of stdlib code to determine that.

Perhaps 40% of the stdlib documents the error type it returns in a message (like 'os.Chdir' always returns '*os.PathError'), but for the rest, good luck. Want to figure out what errors you might have to check for 'tar.Writer.Close()'? Well, the docs says "returns an error", the interface is "error", you have to read hundreds of lines of code to figure out the possible concrete types it could be. Maybe 15% of the time, you end up having to string-match on error messages because the error var or type is unexported.


> interface for external packages to implement so I can plug in logging libraries without rewriting the world.

Well, it's structural, so you don't need other packages to implement an interface rather you need them to accept an interface. That also makes it clear it's a bigger ask - you're not asking a dependency "please also do X" but instead asking "please never need to more than Y".

> Want to figure out what errors you might have to check for 'tar.Writer.Close()'? Well, the docs says "returns an error", the interface is "error", you have to read hundreds of lines of code to figure out the possible concrete types it could be.

The concrete types it could be are unbounded, because `tar.Writer` wraps arbitrary `io.Writer`s. If you need multi-pathed error handling (usually people don't and are just making it out of habit!), worry about what things can do, not what they are.


> If you need multi-pathed error handling (usually people don't and are just making it out of habit!), worry about what things can do, not what they are.

When I'm looking at an error, it's typically for one of two reasons:

1. To set a correct status code, such as http 5xx (internal server error, our disk flaked) or a 4xx (user error, you gave us invalid input).

2. To provide a better error message, such as to localize it into an error string.

If you're building CLI tools for yourself, sure, every error is fatal and you can read english so you don't need either of those. For most go projects, both of those are relevant concerns for a large number of error paths.

Go's type-system does not help you at all.

Speaking of...

> Well, it's structural, so you don't need other packages to implement an interface rather you need them to accept an interface. That also makes it clear it's a bigger ask - you're not asking a dependency "please also do X" but instead asking "please never need to more than Y".

Yup. That is a big problem. That's the root of the error problem too, where every package returns the stdlib error interface, which is a tiny subset of what you usually want.


> To set a correct status code, such as http 5xx (internal server error, our disk flaked) or a 4xx (user error, you gave us invalid input).

For this it's simple to wrap them at return site in something that offers `HTTPStatus() int` and check for implementing that interface, not any concrete types, in your handler.

Also, those error paths should be dangerously hard to mix in the first place, you shouldn't be letting invalid input anywhere near the disk to begin with.

Re. logging interfaces, I think you've missed the point. You want everyone to accept narrow interfaces so you can use the logger you want. You also want everyone to return wide error interfaces so you can categorize the entire universe of possible errors as you want. In the end this isn't a technical problem, it's an "I want everyone to cater for my use case" problem.


> For this it's simple to wrap them at return site in something that offers `HTTPStatus() int` and check for implementing that interface, not any concrete types, in your handler.

It's not though, the return site is inside the go stdlib. I cannot annotate it with new methods.

The only way to figure out how to translate all errors (whether to status codes or to other readable messages in localized languages) is to read the code and figure out what errors it might return.

> you want everyone to return wide error interfaces so you can categorize the entire universe of possible errors as you want. In the end this isn't a technical problem, it's an "I want everyone to cater for my use case" problem.

This is a technical problem. In Rust, libraries define error types and return "Result<T, MyErrorType>", which lets a library author decide what errors are interesting or not. If I think they have not classified an error that is useful for a caller, I can file an issue.

In java, exceptions have types, and I can know what types of checked exceptions a function might throw, and can similarly ask for more specific exceptions, or modify the library to provide them.

In go, _every_ library, due to go's error handling idioms and some mis-features of nils/type-inference, returns the most useless error type possible, the 'error' type, and I have to constantly read docs or code to figure out what types it might be.

I don't see how this isn't a technical issue with the language that, at the type-system level, it makes it an anti-pattern to return concretely typed errors in a way the type system can recognize them.

I don't agree with how you're characterizing what I'm saying as being "catering to my use-case".

Do you just never actually need to classify an error? Is it somehow weird to want to be able to provide a localized error to a user? Doesn't everyone have these problems too?


> It's not though, the return site is inside the go stdlib. I cannot annotate it with new methods.

    type httpClientError struct { error }
    func (err httpClientError) HTTPStatus() int { return 400 }
    func (err httpClientError) Unwrap() error { return err.error } // if needed
It's not even anything special around `error`, Go's entire type scaffolding is built around doing stuff like this.

> Do you just never actually need to classify an error?

Infrequently, and virtually never for errors types I didn't write myself (other than a tiny number of sentinels like UnexpectedEOF or DeadlineExceeded).

> Is it somehow weird to want to be able to provide a localized error to a user?

Yes, it's unusual for error details (rather than e.g. outcomes) to be localized for display directly to non-technical end users. This is also true of exception messages in Java. General-propose desktop client software is rarely written in either language.

I think you're too focused on the specific issue to see my general point about interface size.


You've constructed 'httpClientError', but how do you end up using it in your program? You have "pkg/httputil" or whatever, and the methods in it return "(Response, error)", not "(Response, httpClientError)".

Even within your own program, you now have to read the code in "httputil" to understand what possible error types can be returned.

It's idiomatic to never return concrete error types, whether from the stdlib, or third party libraries, or even methods within your own program.

Even for types you do write yourself, you still have to either memorize what errors each method may return, or you have to constantly refer to docs or source code reading.

Clearly you think this is fine and go's type system is good enough for your use-cases, but every larger go program I've worked with, error handling has been painful since the errors are effectively untyped.

I assume we must have worked on different types of go projects if you haven't run into pain with this.

> Yes, it's unusual for error details (rather than e.g. outcomes) to be localized

I absolutely agree that it's outcomes which are localized, but to determine _outcomes_, you have to classify errors. If the _outcome_ is "File doesn't exist", that's a different error than "permission denied", so you need to classify. But the type you have is "error", so you have to constantly refer to docs.


As I already mentioned - Go has largely unbounded error types, not because `error` is a small interface, but because of how many interfaces get nested (a tar.Writer wrapping a gzip.Writer wrapping an hdfs.Writer etc.). If you don't like this, fine, we'll have to agree to disagree about the value of checked exceptions vs. massively leaking implementation details.

Nonetheless, Go does provide ways to check whether an error either is or can do what you want, and ways to annotate errors with logic specific to your program. An `httpClientError` is an `error`. When you get an error from a source you want to treat as a 400, you wrap it and return it, as an `error`. You use `errors.As` on it as a concrete type, or an `interface { HTTPStatus() int }`, to use the method you've added.

Regarding localization, which is a significantly different problem - the outcome is e.g. "file can't be opened". It's hard to write good error messages based on the language's error messages but this is not a Go-specific problem at all. Either you constrain your operations to the point you can bound all your error types, or you don't and report the outcome + raw message instead of trying to localize causes. And yes, this is an unusual space to be using Go or Java.

Since you mentioned Rust, we could also consider how it solves the problem - `Write` returns a `Result<usize, io::Error>` - `io::Error` has a (almost uselessly long and yet still) non-exhaustive `ErrorKind` - the last of which is `Other`, "used to construct your own Errors that do not match any ErrorKind." I.e. even in Rust's type system, they punted because otherwise you can't easily compose anything.


The main thing that irked me though is I had to spend a whole day doing a simple "take this complex object and use it as a hash key" in an idiomatic way other than just cheating and using the stringified version of the object as the key.


Why do you want a string reverse?

- Do you want to reverse bytes, or codepoints?

- Do you want to reveres codepoints, or grapheme clusters?

- Do you really want to reverse grapheme clusters, or do you want to reverse some grapheme clusters while leaving e.g. sequences of control characters in the same order?

- Do you really want to reify any of this rather than iterate backwards in the existing memory?


I think most people are expecting something along these lines:

C++: reverse(str.begin(), str.end());

Dart: str.split('').reversed.join();

Java: new StringBuilder().append(str).reverse().toString();

JavaScript: str.split('').reverse().join('');

PHP: strrev($str)

Python: ".join(reversed(str))

Rust: str.chars().rev().collect()


Yes but why? Those expressions do wildly different things both in terms of language semantics and in terms of observable behavior and most of them haul in some heavy additional machinery from the language. Perhaps where it is present, this is a case where stdlibs have implemented it because it's easy to implement, and not because it's actually useful.

(What do I mean by wildly different things?

C++: Swaps the string's contents in-place, and probably breaks any multi-byte code units unless you've got a parameterized std::string at hand.

Dart: Makes a new string but has to round-trip via an array, because... it doesn't have a string reverse? This seems like a really bad argument for your side!

Java: Reverses codepoints, but the fact you have to round-trip through a StringBuilder to handle this is also telling.

JavaScript: Same comments as Dart, but I believe this is broken, it will reverse surrogate pairs incorrectly.

PHP: Good luck figuring out what this does depending on your platform, locale, and moon phase.

Python: Another codepoint reverse, again not via strings but a lazy sequence, and also not even idiomatic - use `str[::-1]`.

Rust: And finally again... not a string reverse.

You want a Go slice reverse? You can get a perfect one post-generics.)


And reversing by codepoints is still wrong. It wrecks multi-codepoint sequences like flags.

https://go.dev/play/p/IsZBLqi7--1




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: