| ▲ | blixt 4 days ago |
| I've been using Go more or less in every full-time job I've had since pre-1.0. It's simple for people on the team to pick up the basics, it generally chugs along (I'm rarely worried about updating to latest version of Go), it has most useful things built in, it compiles fast. Concurrency is tricky but if you spend some time with it, it's nice to express data flow in Go. The type system is most of the time very convenient, if sometimes a bit verbose. Just all-around a trusty tool in the belt. But I can't help but agree with a lot of points in this article. Go was designed by some old-school folks that maybe stuck a bit too hard to their principles, losing sight of the practical conveniences. That said, it's a _feeling_ I have, and maybe Go would be much worse if it had solved all these quirks. To be fair, I see more leniency in fixing quirks in the last few years, like at some point I didn't think we'd ever see generics, or custom iterators, etc. The points about RAM and portability seem mostly like personal grievances though. If it was better, that would be nice, of course. But the GC in Go is very unlikely to cause issues in most programs even at very large scale, and it's not that hard to debug. And Go runs on most platforms anyone could ever wish to ship their software on. But yeah the whole error / nil situation still bothers me. I find myself wishing for Result[Ok, Err] and Optional[T] quite often. |
|
| ▲ | xyzzyz 4 days ago | parent | next [-] |
| Go was designed by some old-school folks that maybe stuck a bit too hard to their principles, losing sight of the practical conveniences. I'd say that it's entirely the other way around: they stuck to the practical convenience of solving the problem that they had in front of them, quickly, instead of analyzing the problem from the first principles, and solving the problem correctly (or using a solution that was Not Invented Here). Go's filesystem API is the perfect example. You need to open files? Great, we'll create func Open(name string) (*File, error)
function, you can open files now, done. What if the file name is not valid UTF-8, though? Who cares, hasn't happen to me in the first 5 years I used Go. |
| |
| ▲ | jerf 4 days ago | parent | next [-] | | While the general question about string encoding is fine, unfortunately in a general-purpose and cross-platform language, a file interface that enforces Unicode correctness is actively broken, in that there are files out in the world it will be unable to interact with. If your language is enforcing that, and it doesn't have a fallback to a bag of bytes, it is broken, you just haven't encountered it. Go is correct on this specific API. I'm not celebrating that fact here, nor do I expect the Go designers are either, but it's still correct. | | |
| ▲ | klodolph 4 days ago | parent | next [-] | | This is one of those things that kind of bugs me about, say, OsStr / OsString in Rust. In theory, it’s a very nice, principled approach to strings (must be UTF-8) and filenames (arbitrary bytes, almost, on Linux & Mac). In practice, the ergonomics around OsStr are horrible. They are missing most of the API that normal strings have… it seems like manipulating them is an afterthought, and it was assumed that people would treat them as opaque (which is wrong). Go’s more chaotic approach to allow strings to have non-Unicode contents is IMO more ergonomic. You validate that strings are UTF-8 at the place where you care that they are UTF-8. (So I’m agreeing.) | | |
| ▲ | duckerude 3 days ago | parent | next [-] | | The big problem isn't invalid UTF-8 but invalid UTF-16 (on Windows et al). AIUI Go had nasty bugs around this (https://github.com/golang/go/issues/59971) until it recently adopted WTF-8, an encoding that was actually invented for Rust's OsStr. WTF-8 has some inconvenient properties. Concatenating two strings requires special handling. Rust's opaque types can patch over this but I bet Go's WTF-8 handling exposes some unintuitive behavior. There is a desire to add a normal string API to OsStr but the details aren't settled. For example: should it be possible to split an OsStr on an OsStr needle? This can be implemented but it'd require switching to OMG-WTF-8 (https://rust-lang.github.io/rfcs/2295-os-str-pattern.html), an encoding with even more special cases. (I've thrown my own hat into this ring with OsStr::slice_encoded_bytes().) The current state is pretty sad yeah. If you're OK with losing portability you can use the OsStrExt extension traits. | | |
| ▲ | klodolph 3 days ago | parent [-] | | Yeah, I avoided talking about Windows which isn’t UTF-16 but “int16 string” the same way Unix filenames are int8 strings. IMO the differences with Windows are such that I’m much more unhappy with WTF-8. There’s a lot that sucks about C++ but at least I can do something like #if _WIN32
using pathchar = wchar_t;
constexpr pathchar sep = L'\\';
#else
using pathchar = char;
constexpr pathchar sep = '/';
#endif
using pathstring = std::basic_string<pathchar>;
Mind you this sucks for a lot of reasons, one big reason being that you’re directly exposed to the differences between path representations on different operating systems. Despite all the ways that this (above) sucks, I still generally prefer it over the approaches of Go or Rust. |
| |
| ▲ | Kinrany 3 days ago | parent | prev | next [-] | | > You validate that strings are UTF-8 at the place where you care that they are UTF-8. The problem with this, as with any lack of static typing, is that you now have to validate at _every_ place that cares, or to carefully track whether a value has already been validated, instead of validating once and letting the compiler check that it happened. | | |
| ▲ | klodolph 3 days ago | parent [-] | | In practice, the validation generally happens when you convert to JSON or use an HTML template or something like that, so it’s not so many places. Validation is nice but Rust’s principled approach leaves me high and dry sometimes. Maybe Rust will finish figuring out the OsString interface and at that point we can say Rust has “won” the conversation, but it’s not there yet, and it’s been years. | | |
| ▲ | stouset 3 days ago | parent [-] | | > validation generally happens when Except when it doesn’t and then you have to deal with fucking Cthulhu because everyone thought they could just make incorrect assumptions that aren’t actually enforced anywhere because “oh that never happens”. That isn’t engineering. It’s programming by coincidence. > Maybe Rust will finish figuring out the OsString interface The entire reason OsString is painful to use is because those problems exist and are real. Golang drops them on the floor and forces you pick up the mess on the random day when an unlucky end user loses data. Rust forces you to confront them, as unfortunate as they are. It's painful once, and then the problem is solved for the indefinite future. Rust also provides OsStrExt if you don’t care about portability, which greatly removes many of these issues. I don’t know how that’s not ideal: mistakes are hard, but you can opt into better ergonomics if you don’t need the portability. If you end up needing portability later, the compiler will tell you that you can’t use the shortcuts you opted into. | | |
| ▲ | maxdamantus 3 days ago | parent [-] | | Can you give an example of how Go's approach causes people to lose data? This was alluded to in the blog post but they didn't explain anything. It seems like there's some confusion in the GGGGGP post, since Go works correctly even if the filename is not valid UTF-8 .. maybe that's why they haven't noticed any issues. | | |
| ▲ | xyzzyz 3 days ago | parent [-] | | Imagine that you're writing a function that'll walk the directory to copy some files somewhere else, and then delete the directory. Unfortunately, you hit this https://github.com/golang/go/issues/32334 oops, looks like some files are just inaccessible to you, and you cannot copy them. Fortunately, when you try to delete the source directory, Go's standard library enters infinite loop, which saves your data. https://github.com/golang/go/issues/59971 | | |
| ▲ | maxdamantus 3 days ago | parent | next [-] | | Ah, mentioning Windows filenames would have been useful. I guess the issue isn't so much about whether strings are well-formed, but about whether the conversion (eg, from UTF-16 to UTF-8 at the filesystem boundary) raises an error or silently modifies the data to use replacement characters. I do think that is the main fundamental mistake in Go's Unicode handling; it tends to use replacement characters automatically instead of signalling errors. Using replacement characters is at least conformant to Unicode but imo unless you know the text is not going to be used as an identifier (like a filename), conversion should instead just fail. The other option is using some mechanism to preserve the errors instead of failing quietly (replacement) or failing loudly (raise/throw/panic/return err), and I believe that's what they're now doing for filenames on Windows, using WTF-8. I agree with this new approach, though would still have preferred they not use replacement characters automatically in various places (another one is the "json" module, which quietly corrupts your non-UTF-8 and non-UTF-16 data using replacement characters). Probably worth noting that the WTF-8 approach works because strings are not validated; WTF-8 involves converting invalid UTF-16 data into invalid UTF-8 data such that the conversion is reversible. It would not be possible to encode invalid UTF-16 data into valid UTF-8 data without changing the meaning of valid Unicode strings. | |
| ▲ | klodolph 3 days ago | parent | prev [-] | | IMO the right thing to do here is even messier than Go’s approach, which is give people utf-16-ish strings on Windows. | | |
| ▲ | maxdamantus 3 days ago | parent [-] | | They have effectively done this (since the linked issue was raised), by just converting Windows filenames to WTF-8. I think this is sensible, because the fact that Windows still uses UTF-16 (or more precisely "Unicode 16-bit strings") in some places shouldn't need to complicate the API on other platforms that didn't make the UCS-2/UTF-16 mistake. It's possible that the WTF-8 strings might not concatenate the way they do in UTF-16 or properly enforced WTF-8 (which has special behaviour on concatenation), but they'll still round-trip to the intended 16-bit string, even after concatenation. |
|
|
|
|
|
| |
| ▲ | pas 3 days ago | parent | prev [-] | | It's completely in-line with Rust's approach. Concentrate on the hard stuff that lifts every boat. Like the type system, language features, and keep the standard library very small, and maybe import/adopt very successful packages. (Like once_cell. But since removing things from std is considered a forever no-no, it seems path handling has to be solved by crates. Eg. https://github.com/chipsenkbeil/typed-path ) |
| |
| ▲ | 3 days ago | parent | prev [-] | | [deleted] |
| |
| ▲ | stouset 3 days ago | parent | prev | next [-] | | [flagged] | | |
| ▲ | blibble 3 days ago | parent | next [-] | | > Golang makes it easy to do the dumb, wrong, incorrect thing that looks like it works 99.7% of the time. How can that be wrong? It works in almost all cases! my favorite example of this was the go authors refusing to add monotonic time into the standard library because they confidently misunderstood its necessity (presumably because clocks at google don't ever step) then after some huge outages (due to leap seconds) they finally added it now the libraries are a complete a mess because the original clock/time abstractions weren't built with the concept of multiple clocks and every go program written is littered with terrible bugs due to use of the wrong clock https://github.com/golang/go/issues/12914 (https://github.com/golang/go/issues/12914#issuecomment-15075... might qualify for the worst comment ever) | | |
| ▲ | 0cf8612b2e1e 3 days ago | parent [-] | | This issue is probably my favorite Goism. Real issue identified and the feedback is, “You shouldn’t run hardware that way. Run servers like Google does without time jumping.” Similar with the original stance to code versioning. Just run a monorepo! |
| |
| ▲ | jen20 3 days ago | parent | prev | next [-] | | I can count on fewer hands the number of times I've been bitten by such things in over 10 years of professional Go vs bitten just in the last three weeks by half-assed Java. | | |
| ▲ | gf000 3 days ago | parent | next [-] | | There is a lot to say about Java, but the libraries (both standard lib and popular third-party ones) are goddamn battle-hardened, so I have a hard time believing your claim. | | |
| ▲ | p2detar 3 days ago | parent | next [-] | | They might very well be, because time-handling in Java almost always sucked. In the beginning there was java.util.Date and it was very poorly designed. Sun tried to fix that with java.util.Calendar. That worked for a while but it was still cumbersome, Calendar.getInstance() anyone? After that someone sat down and wrote Joda-Time, which was really really cool and IMO the basis of JSR-310 and the new java.time API. So you're kind of right, but it only took them 15 years to make it right. | | |
| ▲ | gf000 3 days ago | parent [-] | | At the time of Date's "reign", were there any other language with a better library? And Calendar is not a replacement for Date so it's a bit out of the picture. Joda time is an excellent library and indeed it was basically the basis for java's time API, and.. for pretty much any modern language's time API, but given the history - Java basically always had the best time library available at the time. | | |
| ▲ | p2detar 2 days ago | parent [-] | | I’m sorry but I do not agree at all. That “reign” continued forever if you count when java.time got introduced and no, Calendar was not much better in the mean time. Python already had datetime in 2002 or 2003 and VB6 was miles ahead back when Java had just util.Date. |
|
| |
| ▲ | jen20 3 days ago | parent | prev | next [-] | | You can believe what you like, of course, but "battle tested" does not mean "isn't easy to abuse". | |
| ▲ | tom_m 3 days ago | parent | prev [-] | | ROFL really? |
| |
| ▲ | stouset 3 days ago | parent | prev | next [-] | | Is golang better than Java? Sure, fine, maybe. I'm not a Java expert so I don't have a dog in the race. Should and could golang have been so much better than it is? Would golang have been better if Pike and co. had considered use-cases outside of Google, or looked outward for inspiration even just a little? Unambiguously yes, and none of the changes would have needed it to sacrifice its priorities of language simplicity, compilation speed, etc. It is absolutely okay to feel that go is a better language than some of its predecessors while at the same time being utterly frustrated at the the very low-hanging, comparatively obvious, missed opportunities for it to have been drastically better. | |
| ▲ | 3 days ago | parent | prev [-] | | [deleted] |
| |
| ▲ | 0x696C6961 3 days ago | parent | prev | next [-] | | [flagged] | | |
| ▲ | jack_h 3 days ago | parent [-] | | It’s not about making zero mistakes, it’s about learning from previous languages which made mistakes and not repeating them. I decided against using go pretty early on because I recognized just how many mistakes they were repeating that would end up haunting maintainers. | | |
| |
| ▲ | yehyehboi 3 days ago | parent | prev [-] | | [flagged] |
| |
| ▲ | herbstein 4 days ago | parent | prev | next [-] | | Much more egregious is the fact that the API allows returning both an error and a valid file handle. That may be documented to not happen. But look at the Read method instead. It will return both errors and a length you need to handle at the same time. | | |
| ▲ | nasretdinov 4 days ago | parent [-] | | The Read() method is certainly an exception rather than a rule. The common convention is to return nil value upon encountering an error unless there's real value in returning both, e.g. for a partial read that failed in the end but produced some non-empty result nevertheless. It's a rare occasion, yes, but if you absolutely have to handle this case you can. Otherwise you typically ignore the result if err!=nil. It's a mess, true, but real world is also quite messy unfortunately, and Go acknowledges that | | |
| ▲ | stouset 3 days ago | parent [-] | | Go doesn't acknowledge that. It punts. Most of the time if there's a result, there's no error. If there's an error, there's no result. But don't forget to check every time! And make sure you don't make a mistake when you're checking and accidentally use the value anyway, because even though it's technically meaningless it's still nominally a meaningful value since zero values are supposed to be meaningful. Oh and make sure to double-check the docs, because the language can't let you know about the cases where both returns are meaningful. The real world is messy. And golang doesn't give you advance warning on where the messes are, makes no effort to prevent you from stumbling into them, and stands next to you constantly criticizing you while you clean them up by yourself. "You aren't using that variable any more, clean that up too." "There's no new variables now, so use `err =` instead of `err :=`." |
|
| |
| ▲ | koakuma-chan 4 days ago | parent | prev | next [-] | | > What if the file name is not valid UTF-8 Nothing? Neither Go nor the OS require file names to be UTF-8, I believe | | |
| ▲ | zimpenfish 4 days ago | parent | next [-] | | > Nothing? It breaks. Which is weird because you can create a string which isn't valid UTF-8 (eg "\xbd\xb2\x3d\xbc\x20\xe2\x8c\x98") and print it out with no trouble; you just can't pass it to e.g. `os.Create` or `os.Open`. (Bash and a variety of other utils will also complain about it being valid UTF-8; neovim won't save a file under that name; etc.) | | |
| ▲ | yencabulator 3 days ago | parent | next [-] | | That sounds like your kernel refusing to create that file, nothing to do with Go. $ cat main.go
package main
import (
"log"
"os"
)
func main() {
f, err := os.Create("\xbd\xb2\x3d\xbc\x20\xe2\x8c\x98")
if err != nil {
log.Fatalf("create: %v", err)
}
_ = f
}
$ go run .
$ ls -1
''$'\275\262''='$'\274'' ⌘'
go.mod
main.go
| | |
| ▲ | kragen 3 days ago | parent | next [-] | | I've posted a longer explanation in https://news.ycombinator.com/item?id=44991638. I'm interested to hear which kernel and which firesystem zimpenfish is using that has this problem. | | |
| ▲ | yencabulator 2 days ago | parent [-] | | I believe macOS forces UTF-8 filenames and normalizes them to something near-but-not-quite Unicode NFD. Windows doing something similar wouldn't surprise me at all. I believe NTFS internally stores filenames as UTF-16, so enforcing UTF-8 at the API boundary sounds likely. | | |
| ▲ | kragen 2 days ago | parent [-] | | That sounds right. Fortunately, it's not my problem that they're using a buggy piece of shit for an OS. |
|
| |
| ▲ | commandersaki 3 days ago | parent | prev | next [-] | | I'm confused, so is Go restricted to UTF-8 only filenames, because it can read/write arbitrary byte sequences (which is what string can hold), which should be sufficient for dealing with other encodings? | | |
| ▲ | yencabulator 3 days ago | parent [-] | | Go is not restricted, since strings are only conventionally utf-8 but not restricted to that. | | |
| ▲ | commandersaki 3 days ago | parent [-] | | Then I am having a hard time understanding the issue in the post, it seems pretty vague, is there any idea what specific issue is happening, is it how they've used Go, or does Go have an inherent implementation issue, specifically these lines: If you stuff random binary data into a string, Go just steams along, as described in this post. Over the decades I have lost data to tools skipping non-UTF-8 filenames. I should not be blamed for having files that were named before UTF-8 existed. | | |
| ▲ | yencabulator 3 days ago | parent | next [-] | | Let me translate: "I have decided to not like something so now I associate miscellaneous previous negative experiences with it" | |
| ▲ | kragen 3 days ago | parent | prev | next [-] | | The post is wrong on this point, although it's mostly correct otherwise. Just steaming along when you have random binary data in a string, as Golang does, is how you avoid losing data to tools that skip non-UTF-8 filenames, or crash on them. | |
| ▲ | comex 3 days ago | parent | prev [-] | | Yeah, the complaint is pretty bizarre, or at least unclear. |
|
|
| |
| ▲ | zimpenfish 3 days ago | parent | prev [-] | | > That sounds like your kernel refusing to create that file Yes, that was my assumption when bash et al also had problems with it. |
| |
| ▲ | kragen 3 days ago | parent | prev [-] | | It sounds like you found a bug in your filesystem, not in Golang's API, because you totally can pass that string to those functions and open the file successfully. |
| |
| ▲ | johncolanduoni 4 days ago | parent | prev | next [-] | | Well, Windows is an odd beast when 8-bit file names are used. If done naively, you can’t express all valid filenames with even broken UTF-8 and non-valid-Unicode filenames cannot be encoded to UTF-8 without loss or some weird convention. You can do something like WTF-8 (not a misspelling, alas) to make it bidirectional. Rust does this under the hood but doesn’t expose the internal representation. | | |
| ▲ | jstimpfle 4 days ago | parent | next [-] | | What do you mean by "when 8-bit filenames are used"? Do you mean the -A APIs, like CreateFileA()? Those do not take UTF-8, mind you -- unless you are using a relatively recent version of Windows that allows you to run your process with a UTF-8 codepage. In general, Windows filenames are Unicode and you can always express those filenames by using the -W APIs (like CreateFileW()). | | |
| ▲ | af78 4 days ago | parent | next [-] | | I think it depends on the underlying filesystem. Unicode (UTF-16) is first-class on NTFS.
But Windows still supports FAT, I guess, where multiple 8-bit encodings are possible: the so-called "OEM" code pages (437, 850 etc.) or "ANSI" code pages (1250, 1251 etc.). I haven't checked how recent Windows versions cope with FAT file names that cannot be represented as Unicode. | |
| ▲ | johncolanduoni 3 days ago | parent | prev [-] | | Windows filenames in the W APIs are 16-bit (which the A APIs essentially wrap with conversions to the active old-school codepage), and are normally well formed UTF-16. But they aren’t required to be - NTFS itself only cares about 0x0000 and 0x005C (backslash) I believe, and all layers of the stack accept invalid UTF-16 surrogates. Don’t get me started on the normal Win32 path processing (Unicode normalization, “COM” is still a special file, etc.), some of which can be bypassed with the “\\?\” prefix when in NTFS. The upshot is that since the values aren’t always UTF-16, there’s no canonical way to convert them to single byte strings such that valid UTF-16 gets turned into valid UTF-8 but the rest can still be roundtripped. That’s what bastardized encodings like WTF-8 solve. The Rust Path API is the best take on this I’ve seen that doesn’t choke on bad Unicode. |
| |
| ▲ | andyferris 4 days ago | parent | prev [-] | | I believe the same is true on linux, which only cares about 0x2f bytes (i.e. /) | | |
| ▲ | johncolanduoni 3 days ago | parent | next [-] | | Windows paths are not necessarily well-formed UTF-16 (UCS-2 by some people’s definition) down to the filesystem level. If they were always well formed, you could convert to a single byte representation by straightforward Unicode re-encoding. But since they aren’t - there are choices that need to be made about what to do with malformed UTF-16 if you want to round trip them to single byte strings such that they match UTF-8 encoding if they are well formed. In Linux, they’re 8-bit almost-arbitrary strings like you noted, and usually UTF-8. So they always have a convenient 8-bit encoding (I.e. leave them alone). If you hated yourself and wanted to convert them to UTF-16, however, you’d have the same problem Windows does but in reverse. | |
| ▲ | orthoxerox 3 days ago | parent | prev | next [-] | | And 0x00, if I remember correctly. | |
| ▲ | matt_kantor 3 days ago | parent | prev [-] | | And 0x00. |
|
| |
| ▲ | 4 days ago | parent | prev [-] | | [deleted] |
| |
| ▲ | nasretdinov 4 days ago | parent | prev | next [-] | | Note that Go strings can be invalid UTF-8, they dropped panicking on encountering an invalid UTF string before 1.0 I think | | |
| ▲ | xyzzyz 4 days ago | parent [-] | | This also epitomizes the issue. What's the point of having `string` type at all, if it doesn't allow you to make any extra assumptions about the contents beyond `[]byte`? The answer is that they planned to make conversion to `string` error out when it's invalid UTF-8, and then assume that `string`s are valid UTF-8, but then it caused problems elsewhere, so they dropped it for immediate practical convenience. | | |
| ▲ | tialaramex 4 days ago | parent | next [-] | | Rust apparently got relatively close to not having &str as a primitive type and instead only providing a library alias to &[u8] when Rust 1.0 shipped. Score another for Rust's Safety Culture. It would be convenient to just have &str as an alias for &[u8] but if that mistake had been allowed all the safety checking that Rust now does centrally has to be owned by every single user forever. Instead of a few dozen checks overseen by experts there'd be myriad sprinkled across every project and always ready to bite you. | | |
| ▲ | steveklabnik 3 days ago | parent | next [-] | | It wouldn't have been an alias, it would have been struct Str([u8]). Nothing would have been different about the safety story. https://github.com/rust-lang/rfcs/issues/2692 | | |
| ▲ | stouset 3 days ago | parent [-] | | I love this kind of historical knowledge. Thanks for sharing it! |
| |
| ▲ | inferiorhuman 3 days ago | parent | prev | next [-] | | Even so you end up with paper cuts like len which returns the number of bytes. | | |
| ▲ | toast0 3 days ago | parent [-] | | The problem with string length is there's probably at least four concepts that could conceivably be called length, and few people are happy when none of them are len. Of the top of my head, in order of likely difficulty to calculate: byte length, number of code points, number of grapheme/characters, height/width to display. Maybe it would be best for Str not to have len at all. It could have bytes, code_points, graphemes. And every use would be precise. | | |
| ▲ | stouset 3 days ago | parent | next [-] | | > The problem with string length is there's probably at least four concepts that could conceivably be called length. The answer here isn't to throw up your hands, pick one, and other cases be damned. It's to expose them all and let the engineer choose. To not beat the dead horse of Rust, I'll point that Ruby gets this right too. * String#length # count Unicode code units
* String#bytes#length # count bytes
* String#grapheme_clusters#length # count grapheme clusters
Similarly, each of those "views" lets you slice, index, etc. across those concepts naturally. Golang's string is the worst of them all. They're nominally UTF-8, but nothing actually enforces it. But really they're just buckets of bytes, unless you send them to APIs that silently require them to be UTF-8 and drop them on the floor or misbehave if they're not.Height/width to display is font-dependent, so can't just be on a "string" but needs an object with additional context. | |
| ▲ | inferiorhuman 3 days ago | parent | prev | next [-] | | Problems arise when you try to take a slice of a string and end up picking an index (perhaps based on length) that would split a code point. String/str offers an abstraction over Unicode scalars (code points) via the chars iterator, but it all feels a bit messy to have the byte based abstraction more or less be the default. FWIW the docs indicate that working with grapheme clusters will never end up in the standard library. | | |
| ▲ | xyzzyz 3 days ago | parent | next [-] | | You can easily treat `&str` as bytes, just call `.as_bytes()`, and you get `&[u8]`, no questions asked. The reason why you don't want to treat &str as just bytes by default is that it's almost always a wrong thing to do. Moreover, it's the worst kind of a wrong thing, because it actually works correctly 99% of the time, so you might not even realize you have a bug until much too late. If your API takes &str, and tries to do byte-based indexing, it should almost certainly be taking &[u8] instead. | | |
| ▲ | inferiorhuman 3 days ago | parent [-] | | If your API takes &str, and tries to do byte-based indexing, it should
almost certainly be taking &[u8] instead.
Str is indexed by bytes. That's the issue. | | |
| ▲ | xyzzyz 2 days ago | parent [-] | | As a matter of fact, you cannot do let s = “asd”;
println!(“{}”, s[0]);
You will get a compiler error telling you that you cannot index into &str. | | |
| ▲ | inferiorhuman 2 days ago | parent [-] | | Right, you have to give it a usize range. And that will index by bytes. This: fn main() {
let s = "12345";
println!("{}", &s[0..1]);
}
compiles and prints out "1".This: fn main() {
let s = "\u{1234}2345";
println!("{}", &s[0..1]);
}
compiles and panics with the following error: byte index 1 is not a char boundary; it is inside 'ሴ' (bytes 0..3) of `ሴ2345`
To get the nth char (scalar codepoint): fn main() {
let s = "\u{1234}2345";
println!("{}", s.chars().nth(1).unwrap());
}
To get a substring: fn main() {
let s = "\u{1234}2345";
println!("{}", s.chars().skip(0).take(1).collect::<String>());
}
To actually get the bytes you'd have to call #as_bytes which works with scalar and range indices, e.g.: fn main() {
let s = "\u{1234}2345";
println!("{:02X?}", &s.as_bytes()[0..1]);
println!("{:02X}", &s.as_bytes()[0]);
}
IMO it's less intuitive than it should be but still less bad than e.g. Go's two types of nil because it will fail in a visible manner. | | |
| ▲ | xyzzyz 2 days ago | parent [-] | | It's actually somewhat hard to hit that panic in a realistic scenario. This is because you are unlikely to be using slice indices that are not on a character boundary. Where would you even get them from? All the standard library functions will return byte indices on a character boundary. For example, if you try to do something like slice the string between first occurrence of character 'a', and of character 'z', you'll do something like let start = s.find('a')?;
let end = s.find('z')?;
let sub = &s[start..end];
and it will never panic, because find will never return something that's not on a char boundary. | | |
| ▲ | inferiorhuman 2 days ago | parent [-] | | Where would you even get them from?
In my case it was in parsing text where a numeric value had a two character prefix but a string value did not. So I was matching on 0..2 (actually 0..2.min(string.len()) which doubly highlights the indexing issue) which blew up occasionally depending on the string values. There are perhaps smarter ways to do this (e.g. splitn on a space, regex, giant if-else statement, etc, etc) but this seemed at first glance to be the most efficient way because it all fit neatly into a match statement.The inverse was also a problem: laying out text with a monospace font knowing that every character took up the same number of pixels along the x-axis (e.g. no odd emoji or whatever else). Gotta make sure to call #len on #chars instead of the string itself as some of the text (Windows-1250 encoded) got converted into multi-byte Unicode codepoints. |
|
|
|
|
| |
| ▲ | toast0 3 days ago | parent | prev [-] | | > but it all feels a bit messy to have the byte based abstraction more or less be the default. I mean, really neither should be the default. You should have to pick chars or bytes on use, but I don't think that's palatable; most languages have chosen one or the other as the preferred form. Or some have the joy of being forward thinking in the 90s and built around UCS-2 and later extended to UTF-16, so you've got 16-bit 'characters' with some code points that are two characters. Of course, dealing with operating systems means dealing with whatever they have as well as what the language prefers (or, as discussed elsewhere in this thread, pretending it doesn't exist to make easy things easier and hard things harder) |
| |
| ▲ | branko_d 3 days ago | parent | prev [-] | | You could also have the number of code UNITS, which is the route C# took. |
|
| |
| ▲ | adastra22 3 days ago | parent | prev [-] | | . (early morning brain fart -- I need my coffee) | | |
| ▲ | tialaramex 3 days ago | parent [-] | | So it's true that technically the primitive type is str, and indeed it's even possible to make a &mut str though it's quite rare that you'd want to mutably borrow the string slice. However no &str is not "an alias for &&String" and I can't quite imagine how you'd think that. String doesn't exist in Rust's core, it's from alloc and thus wouldn't be available if you don't have an allocator. | | |
| ▲ | zozbot234 3 days ago | parent [-] | | str is not really a "primitive type", it only exists abstractly as an argument to type constructors - treating the & operator as a "type constructor" for that purpose, but including Box<>, Rc<>, Arc<> etc. So you can have Box<str> or Arc<str> in addition to &str or perhaps &mut str, but not really 'str' in isolation. |
|
|
| |
| ▲ | 0x000xca0xfe 4 days ago | parent | prev | next [-] | | Why not use utf8.ValidString in the places it is needed? Why burden one of the most basic data types with highly specific format checks? It's far better to get some � when working with messy data instead of applications refusing to work and erroring out left and right. | | |
| ▲ | const_cast 3 days ago | parent [-] | | IMO utf8 isn't a highly specific format, it's universal for text. Every ascii string you'd write in C or C++ or whatever is already utf8. So that means that for 99% of scenarios, the difference between char[] and a proper utf8 string is none. They have the same data representation and memory layout. The problem comes in when people start using string like they use string in PHP. They just use it to store random bytes or other binary data. This makes no sense with the string type. String is text, but now we don't have text. That's a problem. We should use byte[] or something for this instead of string. That's an abuse of string. I don't think allowing strings to not be text is too constraining - that's what a string is! | | |
| ▲ | kragen 3 days ago | parent | next [-] | | The approach you are advocating is the approach that was abandoned, for good reasons, in the Unix filesystem in the 70s and in Perl in the 80s. One of the great advances of Unix was that you don't need separate handling for binary data and text data; they are stored in the same kind of file and can be contained in the same kinds of strings (except, sadly, in C). Occasionally you need to do some kind of text-specific processing where you care, but the rest of the time you can keep all your code 8-bit clean so that it can handle any data safely. Languages that have adopted the approach you advocate, such as Python, frequently have bugs like exception tracebacks they can't print (because stdout is set to ASCII) or filenames they can't open when they're passed in on the command line (because they aren't valid UTF-8). | | | |
| ▲ | adastra22 3 days ago | parent | prev [-] | | Not all text is UTF-8, and there are real world contexts (e.g. Windows) where this matters a lot. | | |
| ▲ | const_cast 3 days ago | parent [-] | | Yes, Windows text is broken in its own special way. We can try to shove it into objects that work on other text but this won't work in edge cases. Like if I take text on Linux and try to write a Windows file with that text, it's broken. And vice versa. Go let's you do the broken thing. In Rust or even using libraries in most languages, you cant. You have to specifically convert between them. That's why I mean when I say "storing random binary data as text". Sure, Windows almost UTF16 abomination is kind of text, but not really. Its its own thing. That requires a different type of string OR converting it to a normal string. | | |
| ▲ | adastra22 3 days ago | parent [-] | | Even on Linux, you can't have '/' in a filename, or ':' on macOS. And this is without getting into issues related to the null byte in strings. Having a separate Path object that represents a filename or path + filename makes sense, because on every platform there are idiosyncratic requirements surrounding paths. It maybe legacy cruft downstream of poorly thought out design decisions at the system/OS level, but we're stuck with it. And a language that doesn't provide the tooling necessary to muddle through this mess safely isn't a serious platform to build on, IMHO. There is room for languages that explicitly make the tradeoff of being easy to use (e.g. a unified string type) at the cost of not handling many real world edge cases correctly. But these should not be used for serious things like backup systems where edge cases result in lost data. Go is making the tradeoff for language simplicity, while being marketed and positioned as a serious language for writing serious programs, which it is not. | | |
| ▲ | const_cast 3 days ago | parent [-] | | > Even on Linux, you can't have '/' in a filename, or ':' on macOS Yes this is why all competent libraries don't actually use string for path. They have their own path data type because it's actually a different data type. Again, you can do the Go thing and just use the broken string, but that's dumb and you shouldn't. They should look at C++ std::filesystem, it's actually quite good in this regard. > And a language that doesn't provide the tooling necessary to muddle through this mess safely isn't a serious platform to build on, IMHO. I agree, even PHP does a better job at this than Go, which is really saying something. > Go is making the tradeoff for language simplicity, while being marketed and positioned as a serious language for writing serious programs, which it is not. I would agree. | | |
| ▲ | astrange 3 days ago | parent [-] | | > Yes this is why all competent libraries don't actually use string for path. They have their own path data type because it's actually a different data type. What is different about it? I don't see any constraints here relevant to having a different type. Note that this thread has already confused the issue, because they said filename and you said path. A path can contain /, it just happens to mean something. If you want a better abstraction to locations of files on disk, then you shouldn't use paths at all, since they break if the file gets moved. | | |
| ▲ | const_cast 3 days ago | parent [-] | | A string can contain characters a path cannot, depending on the operating system. So only some strings are valid paths. Typically the way you do this is you have the constructor for path do the validation or you use a static path::fromString() function. Also paths breaking when a file is moved is correct behavior sometimes. For example something like openFile() or moveFile() requires paths. Also path can be relative location. | | |
| ▲ | astrange 3 days ago | parent [-] | | > A string can contain characters a path cannot, depending on the operating system. So only some strings are valid paths. Can it? If you want to open a file with invalid UTF8 in the name, then the path has to contain that. And a path can contain the path separator - it's the filename that can't contain it. > For example something like openFile() or moveFile() requires paths. macOS has something called bookmark URLs that can contain things like inode numbers or addresses of network mounts. Apps use it to remember how to find recently opened files even if you've reorganized your disk or the mount has dropped off. IIRC it does resolve to a path so it can use open() eventually, but you could imagine an alternative. Well, security issues aside. | | |
| ▲ | adastra22 3 days ago | parent [-] | | Rust allows null bytes in str. Most (all?) OS don't allow null bytes in filenames. |
|
|
|
|
|
|
|
|
| |
| ▲ | assbuttbuttass 4 days ago | parent | prev | next [-] | | string is just an immutable []byte. It's actually one of my favorite things about Go that strings can contain invalid utf-8, so you don't end up with the Rust mess of String vs OSString vs PathBuf vs Vec<u8>. It's all just string | | |
| ▲ | zozbot234 4 days ago | parent [-] | | Rust &str and String are specifically intended for UTF-8 valid text. If you're working with arbitrary byte sequences, that's what &[u8] and Vec<u8> are for in Rust. It's not a "mess", it's just different from what Golang does. | | |
| ▲ | gf000 4 days ago | parent | next [-] | | If anything that will make Rust programs likely to be correct under any strange text input, while Go might just handle the happy path of ASCII inputs. Stuff like this matters a great deal on the standard library level. | | | |
| ▲ | maxdamantus 4 days ago | parent | prev [-] | | It's never been clear to me where such a type is actually useful. In what cases do you really need to restrict it to valid UTF-8? You should always be able to iterate the code points of a string, whether or not it's valid Unicode. The iterator can either silently replace any errors with replacement characters, or denote the errors by returning eg, `Result<char, Utf8Error>`, depending on the use case. All languages that have tried restricting Unicode afaik have ended up adding workarounds for the fact that real world "text" sometimes has encoding errors and it's often better to just preserve the errors instead of corrupting the data through replacement characters, or just refusing to accept some inputs and crashing the program. In Rust there's bstr/ByteStr (currently being added to std), awkward having to decide which string type to use. In Python there's PEP-383/"surrogateescape", which works because Python strings are not guaranteed valid (they're potentially ill-formed UTF-32 sequences, with a range restriction). Awkward figuring out when to actually use it. In Raku there's UTF8-C8, which is probably the weirdest workaround of all (left as an exercise for the reader to try to understand .. oh, and it also interferes with valid Unicode that's not normalized, because that's another stupid restriction). Meanwhile the Unicode standard itself specifies Unicode strings as being sequences of code units [0][1], so Go is one of the few modern languages that actually implements Unicode (8-bit) strings. Note that at least two out of the three inventors of Go also basically invented UTF-8. [0] https://www.unicode.org/versions/Unicode16.0.0/core-spec/cha... > Unicode string: A code unit sequence containing code units of a particular Unicode encoding form. [1] https://www.unicode.org/versions/Unicode16.0.0/core-spec/cha... > Unicode strings need not contain well-formed code unit sequences under all conditions. This is equivalent to saying that a particular Unicode string need not be in a Unicode encoding form. | | |
| ▲ | empath75 3 days ago | parent | next [-] | | > It's never been clear to me where such a type is actually useful. In what cases do you really need to restrict it to valid UTF-8? Because 99.999% of the time you want it to be valid and would like an error if it isn't? If you want to work with invalid UTF-8, that should be a deliberate choice. | | |
| ▲ | maxdamantus 3 days ago | parent [-] | | Do you want grep to crash when your text file turned out to have a partially written character in it? 99.999% seems very high, and you haven't given an actual use case for the restriction. | | |
| ▲ | empath75 3 days ago | parent | next [-] | | Rust doesn't crash when it gets an error unless you tell it to. You make a choice how to handle the error because you have to it or it won't compile. If you don't care about losing information when reading a file, you can use the lossy function that gracefully handles invalid bytes. | |
| ▲ | gf000 3 days ago | parent | prev [-] | | Crash? No. But I can safely handle the error where it happens, because the language actually helps me with this situation by returning a proper Result type. So I have to explicitly check which "variant" I have, instead of forgetting to call the validate function in case of go. |
|
| |
| ▲ | xyzzyz 3 days ago | parent | prev [-] | | The way Rust handles this is perfectly fine. String type promises its contents are valid UTF-8. When you create it from array of bytes, you have three options: 1) ::from_utf8, which will force you to handle invalid UTF-8 error, 2) ::from_utf8_lossy, which will replace invalid code points with replacement character code point, and 3) from_utf8_unchecked, which will not do the validity check and is explicitly marked as unsafe. | | |
| ▲ | maxdamantus 3 days ago | parent [-] | | But there's no option to just construct the string with the invalid bytes. 3) is not for this purpose; it is for when you already know that it is valid. If you use 3) to create a &str/String from invalid bytes, you can't safely use that string as the standard library is unfortunately designed around the assumption that only valid UTF-8 is stored. https://doc.rust-lang.org/std/primitive.str.html#invariant > Constructing a non-UTF-8 string slice is not immediate undefined behavior, but any function called on a string slice may assume that it is valid UTF-8, which means that a non-UTF-8 string slice can lead to undefined behavior down the road. | | |
| ▲ | gf000 3 days ago | parent | next [-] | | How could any library function work with completely random bytes? Like, how would it iterate over code points? It may want to assume utf8's standard rules and e.g. know that after this byte prefix, the next byte is also part of the same code point (excuse me if I'm using wrong terminology), but now you need complex error handling at every single line, which would be unnecessary if you just made your type represent only valid instances. Again, this is the same simplistic, vs just the right abstraction, this just smudges the complexity over a much larger surface area. If you have a byte array that is not utf-8 encoded, then just... use a byte array. | | |
| ▲ | kragen 3 days ago | parent [-] | | There are a lot of operations that are valid and well-defined on binary strings, such as sorting them, hashing them, writing them to files, measuring their lengths, indexing a trie with them, splitting them on delimiter bytes or substrings, concatenating them, substring-searching them, posting them to ZMQ as messages, subscribing to them as ZMQ prefixes, using them as keys or values in LevelDB, and so on. For binary strings that don't contain null bytes, we can add passing them as command-line arguments and using them as filenames. The entire point of UTF-8 (designed, by the way, by the group that designed Go) is to encode Unicode in such a way that these byte string operations perform the corresponding Unicode operations, precisely so that you don't have to care whether your string is Unicode or just plain ASCII, so you don't need any error handling, except for the rare case where you want to do something related to the text that the string semantically represents. The only operation that doesn't really map is measuring the length. | | |
| ▲ | xyzzyz 3 days ago | parent | next [-] | | > There are a lot of operations that are valid and well-defined on binary strings, such as (...), and so on. Every single thing you listed here is supported by &[u8] type. That's the point: if you want to operate on data without assuming it's valid UTF-8, you just use &[u8] (or allocating Vec<u8>), and the standard library offers what you'd typically want, except of the functions that assume that the string is valid UTF-8 (like e.g. iterating over code points). If you want that, you need to convert your &[u8] to &str, and the process of conversion forces you to check for conversion errors. | | |
| ▲ | maxdamantus 3 days ago | parent | next [-] | | The problem is that there are so many functions that unnecessarily take `&str` rather than `&[u8]` because the expectation is that textual things should use `&str`. So you naturally write another one of these functions that takes a `&str` so that it can pass to another function that only accepts `&str`. Fundamentally no one actually requires validation (ie, walking over the string an extra time up front), we're just making it part of the contract because something else has made it part of the contract. | | |
| ▲ | kragen 3 days ago | parent [-] | | It's much worse than that—in many cases, such as passing a filename to a program on the Linux command line, correct behavior requires not validating, so erroring out when validation fails introduces bugs. I've explained this in more detail in https://news.ycombinator.com/item?id=44991638. |
| |
| ▲ | kragen 3 days ago | parent | prev [-] | | That's semantically okay, but giving &str such a short name creates a dangerous temptation to use it for things such as filenames, stdio, and command-line arguments, where that process of conversion introduces errors into code that would otherwise work reliably for any non-null-containing string, as it does in Go. If it were called something like ValidatedUnicodeTextSlice it would probably be fine. | | |
| ▲ | adastra22 3 days ago | parent | next [-] | | I'd agree if it was &[bytes] or whatever. But &[u8] isn't that much different from &str. | | |
| ▲ | kragen 3 days ago | parent [-] | | Isn't &[u8] what you should be using for command-line arguments and filenames and whatnot? In that case you'd want its name to be short, like &[u8], rather than long like &[bytes] or &[raw_uncut_byte] or something. | | |
| ▲ | adastra22 3 days ago | parent [-] | | OsStr/OsString is what you would use in those circumstances. Path/PathBuf specifically for filenames or paths, which I think uses OsStr/OsString internally. I've never looked at OsStr's internals but I wouldn't be surprised if it is a wrapper around &[u8]. Note that &[u8] would allow things like null bytes, and maybe other edge cases. | | |
| ▲ | kragen 3 days ago | parent [-] | | You can't get null bytes from a command-line argument. And going by https://news.ycombinator.com/item?id=44991638 it's common to not use OsString when accepting command-line arguments, because std::env::args yields Strings, which means that probably most Rust programs that accept filenames on the command line have this bug. | | |
| ▲ | adastra22 3 days ago | parent [-] | | Rust String can contain null bytes! Rust uses explicit string lengths. Agree though that most OS wouldn't be able to pass null bytes in arguments though. | | |
| ▲ | kragen 3 days ago | parent [-] | | Right, but it can't contain invalid UTF-8, which is valid in both command-line parameters and in filenames on Linux, FreeBSD, and other normal Unixes. See my link above for a demonstration of how this causes bugs in Rust programs. |
|
|
|
|
| |
| ▲ | xyzzyz 3 days ago | parent | prev [-] | | It's actually extremely hard to introduce problems like that, precisely because Rust's standard library is very well designed. Can you give an example scenario where it would be a problem? | | |
| ▲ | kragen 3 days ago | parent [-] | | Well, for example, the extremely exotic scenario of passing command-line arguments to a program on little-known operating systems like Linux and FreeBSD; https://doc.rust-lang.org/book/ch12-01-accepting-command-lin... recommends: use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
...
}
When I run this code, a literal example from the official manual, with this filename I have here, it panics: $ ./main $'\200'
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: "\x80"', library/std/src/env.rs:805:51
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
($'\200' is bash's notation for a single byte with the value 128. We'll see it below in the strace output.)So, literally any program anyone writes in Rust will crash if you attempt to pass it that filename, if it uses the manual's recommended way to accept command-line arguments. It might work fine for a long time, in all kinds of tests, and then blow up in production when a wild file appears with a filename that fails to be valid Unicode. This C program I just wrote handles it fine: #include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
char buf[4096];
void
err(char *s)
{
perror(s);
exit(-1);
}
int
main(int argc, char **argv)
{
int input, output;
if ((input = open(argv[1], O_RDONLY)) < 0) err(argv[1]);
if ((output = open(argv[2], O_WRONLY | O_CREAT, 0666)) < 0) err(argv[2]);
for (;;) {
ssize_t size = read(input, buf, sizeof buf);
if (size < 0) err("read");
if (size == 0) return 0;
ssize_t size2 = write(output, buf, (size_t)size);
if (size2 != size) err("write");
}
}
(I probably should have used O_TRUNC.)Here you can see that it does successfully copy that file: $ cat baz
cat: baz: No such file or directory
$ strace -s4096 ./cp $'\200' baz
execve("./cp", ["./cp", "\200", "baz"], 0x7ffd7ab60058 /* 50 vars */) = 0
brk(NULL) = 0xd3ec000
brk(0xd3ecd00) = 0xd3ecd00
arch_prctl(ARCH_SET_FS, 0xd3ec380) = 0
set_tid_address(0xd3ec650) = 4153012
set_robust_list(0xd3ec660, 24) = 0
rseq(0xd3ecca0, 0x20, 0, 0x53053053) = 0
prlimit64(0, RLIMIT_STACK, NULL, {rlim_cur=9788*1024, rlim_max=RLIM64_INFINITY}) = 0
readlink("/proc/self/exe", ".../cp", 4096) = 22
getrandom("\xcf\x1f\xb7\xd3\xdb\x4c\xc7\x2c", 8, GRND_NONBLOCK) = 8
brk(NULL) = 0xd3ecd00
brk(0xd40dd00) = 0xd40dd00
brk(0xd40e000) = 0xd40e000
mprotect(0x4a2000, 16384, PROT_READ) = 0
openat(AT_FDCWD, "\200", O_RDONLY) = 3
openat(AT_FDCWD, "baz", O_WRONLY|O_CREAT, 0666) = 4
read(3, "foo\n", 4096) = 4
write(4, "foo\n", 4) = 4
read(3, "", 4096) = 0
exit_group(0) = ?
+++ exited with 0 +++
$ cat baz
foo
The Rust manual page linked above explains why they think introducing this bug by default into all your programs is a good idea, and how to avoid it:> Note that std::env::args will panic if any argument contains invalid Unicode. If your program needs to accept arguments containing invalid Unicode, use std::env::args_os instead. That function returns an iterator that produces OsString values instead of String values. We’ve chosen to use std::env::args here for simplicity because OsString values differ per platform and are more complex to work with than String values. I don't know what's "complex" about OsString, but for the time being I'll take the manual's word for it. So, Rust's approach evidently makes it extremely hard not to introduce problems like that, even in the simplest programs. Go's approach doesn't have that problem; this program works just as well as the C program, without the Rust footgun: package main
import (
"io"
"log"
"os"
)
func main() {
src, err := os.Open(os.Args[1])
if err != nil {
log.Fatalf("open source: %v", err)
}
dst, err := os.OpenFile(os.Args[2], os.O_CREATE|os.O_WRONLY, 0666)
if err != nil {
log.Fatalf("create dest: %v", err)
}
if _, err := io.Copy(dst, src); err != nil {
log.Fatalf("copy: %v", err)
}
}
(O_CREATE makes me laugh. I guess Ken did get to spell "creat" with an "e" after all!)This program generates a much less clean strace, so I am not going to include it. You might wonder how such a filename could arise other than as a deliberate attack. The most common scenario is when the filenames are encoded in a non-Unicode encoding like Shift-JIS or Latin-1, followed by disk corruption, but the deliberate attack scenario is nothing to sneeze at either. You don't want attackers to be able to create filenames your tools can't see, or turn to stone if they examine, like Medusa. Note that the log message on error also includes the ill-formed Unicode filename: $ ./cp $'\201' baz
2025/08/22 21:53:49 open source: open ζ: no such file or directory
But it didn't say ζ. It actually emitted a byte with value 129, making the error message ill-formed UTF-8. This is obviously potentially dangerous, depending on where that logfile goes because it can include arbitrary terminal escape sequences. But note that Rust's UTF-8 validation won't protect you from that, or from things like this: $ ./cp $'\n2025/08/22 21:59:59 oh no' baz
2025/08/22 21:59:09 open source: open
2025/08/22 21:59:59 oh no: no such file or directory
I'm not bagging on Rust. There are a lot of good things about Rust. But its string handling is not one of them. | | |
| ▲ | anarki8 3 days ago | parent [-] | | There might be potential improvements, like using OsString by default for `env::args()` but I would pick Rust's string handling over Go’s or C's any day. | | |
| ▲ | kragen 3 days ago | parent [-] | | It's reasonable to argue that C's string handling is as bad as Rust's, or worse. |
|
|
|
|
| |
| ▲ | gf000 3 days ago | parent | prev [-] | | Then [u8] can surely implement those functions. |
|
| |
| ▲ | adastra22 3 days ago | parent | prev | next [-] | | I don’t understand this complaint. (3) sounds like exactly what you are asking for. And yes, doing unsafe thing is unsafe. | | |
| ▲ | maxdamantus 3 days ago | parent [-] | | > I don’t understand this complaint. (3) sounds like exactly what you are asking for. And yes, doing unsafe thing is unsafe You're meant to use `unsafe` as a way of limiting the scope of reasoning about safety. Once you construct a `&str` using `from_utf8_unchecked`, you can't safely pass it to any other function without looking at its code and reasoning about whether it's still safe. Also see the actual documentation: https://doc.rust-lang.org/std/primitive.str.html#method.from... > Safety: The bytes passed in must be valid UTF-8. |
| |
| ▲ | xyzzyz 3 days ago | parent | prev [-] | | > If you use 3) to create a &str/String from invalid bytes, you can't safely use that string as the standard library is unfortunately designed around the assumption that only valid UTF-8 is stored. Yes, and that's a good thing. It allows every code that gets &str/String to assume that the input is valid UTF-8. The alternative would be that every single time you write a function that takes a string as an argument, you have to analyze your code, consider what would happen if the argument was not valid UTF-8, and handle that appropriately. You'd also have to redo the whole analysis every time you modify the function. That's a horrible waste of time: it's much better to: 1) Convert things to String early, and assume validity later, and 2) Make functions that explicitly don't care about validity take &[u8] instead. This is, of course, exactly what Rust does: I am not aware of a single thing that &str allows you to do that you cannot do with &[u8], except things that do require you to assume it's valid UTF-8. | | |
| ▲ | maxdamantus 3 days ago | parent [-] | | > This is, of course, exactly what Rust does: I am not aware of a single thing that &str allows you to do that you cannot do with &[u8], except things that do require you to assume it's valid UTF-8. Doesn't this demonstrate my point? If you can do everything with &[u8], what's the point in validating UTF-8? It's just a less universal string type, and your program wastes CPU cycles doing unnecessary validation. | | |
| ▲ | matt_kantor 3 days ago | parent [-] | | > except things that do require you to assume it's valid UTF-8 That's the point. | | |
| ▲ | maxdamantus 3 days ago | parent [-] | | But no one has demonstrated an actual operation that requires valid UTF-8. The reasoning is always circular: "I require valid UTF-8 because someone else requires valid UTF-8". Eventually there should be an underlying operation which can only work on valid UTF-8, but that doesn't exist. UTF-8 was designed such that invalid data can be detected and handled, without affecting the meaning of valid subsequences in the same string. |
|
|
|
|
|
|
|
| |
| ▲ | roncesvalles 4 days ago | parent | prev | next [-] | | I've always thought the point of the string type was for indexing. One index of a string is always one character, but characters are sometimes composed of multiple bytes. | | |
| ▲ | crazygringo 4 days ago | parent | next [-] | | Yup. But to be clear, in Unicode a string will index code points, not characters. E.g. a single emoji can be made of multiple code points, as well as certain characters in certain languages. The Unicode name for a character like this is a "grapheme", and grapheme splitting is so complicated it generally belongs in a dedicated Unicode library, not a general-purpose string object. | |
| ▲ | birn559 4 days ago | parent | prev [-] | | You can't do that in a performant way and going that route can lead to problems, because characters (= graphemes in the language of Unicode) generally don't always behave as developers assume. |
| |
| ▲ | naikrovek 4 days ago | parent | prev [-] | | I think maybe you've forgotten about the rune type. Rune does make assumptions. []Rune is for sequences of UTF characters. rune is an alias for int32. string, I think, is an alias for []byte. | | |
| ▲ | TheDong 3 days ago | parent [-] | | `string` is not an alias for []byte. Consider: for i, chr := range string([]byte{226, 150, 136, 226, 150, 136}) {
fmt.Printf("%d = %v\n", i, chr)
// note, s[i] != chr
}
How many times does that loop over 6 bytes iterate? The answer is it iterates twice, with i=0 and i=3.There's also quite a few standard APIs that behave weirdly if a string is not valid utf-8, which wouldn't be the case if it was just a bag of bytes. | | |
| ▲ | naikrovek 15 hours ago | parent [-] | | Go programmers (and `range`) assume that string is always valid UTF-8 but there is no guarantee by the language that a string is valid UTF-8. The string itself is still a []byte. `range` sees the `string` type and has special handling for strings that it does not have when it ranges over []byte. Recall that aliased types are not viewed as the same type at any time. A couple quotes from the Go Blog by Rob Pike: > It’s important to state right up front that a string holds arbitrary bytes. It is not required to hold Unicode text, UTF-8 text, or any other predefined format. As far as the content of a string is concerned, it is exactly equivalent to a slice of bytes. > Besides the axiomatic detail that Go source code is UTF-8, there’s really only one way that Go treats UTF-8 specially, and that is when using a for range loop on a string. Both from https://go.dev/blog/strings If you want UTF-8 in a guaranteed way, use the functions available in unicode/utf8 for that. Using `string` is not sufficient unless you make sure you only put UTF-8 into those strings. If you put valid UTF-8 into a string, you can be sure that the string holds valid UTF-8, but if someone else puts data into a string, and you assume that it is valid UTF-8, you may have a problem because of that assumption. |
|
|
|
| |
| ▲ | silverwind 4 days ago | parent | prev | next [-] | | > What if the file name is not valid UTF-8, though They could support passing filename as `string | []byte`. But wait, go does not even have union types. | | |
| ▲ | lblume 4 days ago | parent [-] | | But []byte, or a wrapper like Path, is enough, if strings are easily convertible into it. Rust does it that way via the AsRef<T> trait. |
| |
| ▲ | kragen 3 days ago | parent | prev | next [-] | | If the filename is not valid UTF-8, Golang can still open the file without a problem, as long as your filesystem doesn't attempt to be clever. Linux ext4fs and Go both consider filenames to be binary strings except that they cannot contain NULs. This is one of the minor errors in the post. | |
| ▲ | ants_everywhere 4 days ago | parent | prev | next [-] | | > they stuck to the practical convenience of solving the problem that they had in front of them, quickly, instead of analyzing the problem from the first principles, and solving the problem correctly (or using a solution that was Not Invented Here). I've said this before, but much of Go's design looks like it's imitating the C++ style at Google. The comments where I see people saying they like something about Go it's often an idiom that showed up first in the C++ macros or tooling. I used to check this before I left Google, and I'm sure it's becoming less true over time. But to me it looks like the idea of Go was basically "what if we created a Python-like compiled language that was easier to onboard than C++ but which still had our C++ ergonomics?" | | |
| ▲ | shrubble 4 days ago | parent [-] | | Didn’t Go come out of a language that was written for Plan9, thus pre-dating Rob Pike’s work at Google? | | |
| ▲ | pjmlp 3 days ago | parent | next [-] | | Kind of, Limbo, written for Inferno, taking into consideration what made Alef's design for Plan 9 a failure, like not having garbage collection. | |
| ▲ | kragen 3 days ago | parent | prev | next [-] | | Yes, Golang is superficially almost identical to Pike's Newsqueak. | | | |
| ▲ | ants_everywhere 3 days ago | parent | prev [-] | | not that I recall but I may not be recalling correctly. But certainly, anyone will bring their previous experience to the project, so there must be some Plan9 influence in there somewhere | | |
| ▲ | kragen 3 days ago | parent [-] | | They were literally using the Plan9 C compiler and linker. | | |
| ▲ | ants_everywhere 3 days ago | parent [-] | | Yes I'm aware | | |
| ▲ | kragen 3 days ago | parent [-] | | Literally building the project out of the Plan 9 source code is very far from "bring[ing] their previous experience to the project, (...) some Plan9 influence in there somewhere" | | |
| ▲ | ants_everywhere 3 days ago | parent [-] | | It's a C compiler. Is your point that Go is influenced by C? ... | | |
| ▲ | tom_m 3 days ago | parent | next [-] | | They started there, but it now is compiled by go itself. | |
| ▲ | kragen 3 days ago | parent | prev [-] | | I think you should upgrade to a less badly quantized neural network model. | | |
| ▲ | ants_everywhere 3 days ago | parent [-] | | I don't see why you've been continually replying so impolitely. I've tried to give you the benefit of the doubt, but I see I've just wasted my time. | | |
|
|
|
|
|
|
|
| |
| ▲ | 3 days ago | parent | prev | next [-] | | [deleted] | |
| ▲ | perryizgr8 3 days ago | parent | prev [-] | | > What if the file name is not valid UTF-8, though? Then make it valid UTF-8. If you try to solve the long tail of issues in a commonly used function of the library its going to cause a lot of pain. This approach is better. If someone has a weird problem like file names with invalid characters, they can solve it themselves, even publish a package. Why complicate 100% of uses for solving 0.01% of issues? | | |
| ▲ | nomel 3 days ago | parent [-] | | > Then make it valid UTF-8. I think you misunderstand. How do you do that for a file that exists on disk that's trying to be read? Rename it for them? They may not like that. |
|
|
|
| ▲ | xtracto 4 days ago | parent | prev | next [-] |
| I recently started writing Go for a new job, after 20 years of not touching a compiled language for something serious (I've done DevKitArm dev. as a hobby). I know it's mostly a matter of tastes, but darn, it feels horrible. And there are no default parameter values, and the error hanling smells bad, and no real stack trace in production. And the "object orientation" syntax, adding some ugly reference to each function. And the pointers... It took me back to my C/C++ days. Like programming with 25 year old technology from back when I was in university in 1999. |
| |
| ▲ | pjmlp 4 days ago | parent | next [-] | | And then people are amazed for it to achieve compile times, compiled languages were already doing on PCs running at 10 MHz within the constraints of 640 KB (TB, TP, Modula-2, Clipper, QB). | | |
| ▲ | remus 4 days ago | parent | next [-] | | > [some] compiled languages were already doing on PCs running at 10 MHz within the constraints of 640 KB Many compiled languages are very slow to compile however, especially for large projects, C++ and rust being the usual examples. | | |
| ▲ | adastra22 3 days ago | parent | next [-] | | It is weird to lump C++ and Rust together. I have used Rust code bases that compile in 2-3 minutes what a C++ compiler would take literally hours to compile. I feel people who complain about rustc compile times must be new to using compiled languages… | | |
| ▲ | pjmlp 3 days ago | parent | next [-] | | There is a way to make C++ beat Rust though. Make use of binary libraries, export templates, incremental compilation and linking with multiple cores, and if using VC++ or clang vLatest, modules. It still isn't Delphi fast, but becomes more manageable. | |
| ▲ | surajrmal 2 days ago | parent | prev [-] | | If you use lto, rust compilation is far worse for comparable c++ code. |
| |
| ▲ | pjmlp 4 days ago | parent | prev | next [-] | | True, however there are more programming languages than only C++ and Rust. | |
| ▲ | gf000 4 days ago | parent | prev [-] | | Well, spewing out barely-optimized machine code and having an ultra-weak type system certainly helps with speed - a la Go! | | |
| ▲ | remus 4 days ago | parent [-] | | That's a reasonable trade-off to make for some people, no? There's plenty of work to be done where you can cope with the occasional runtime error and less then bleeding edge performance, especially if that then means wins in other areas (compile speeds, tooling). Having a variety of languages available feels like a pretty good thing to me. | | |
| ▲ | gf000 4 days ago | parent | next [-] | | Well, I personally would be happier with a stronger type system (e.g. java can compile just as fast, and it has a less anemic type system), but sure. And sure, it is welcome from a dev POV on one hand, though from an ecosystem perspective, more languages are not necessarily good as it multiplies the effort required. | | |
| ▲ | pjmlp 3 days ago | parent | next [-] | | It is kind of ironic that from Go's point of view, Java's type system is PhD level of language knowledge. Especially given how the language was criticised back in 1996. | |
| ▲ | Mawr 3 days ago | parent | prev [-] | | What do you mean by saying Java compiles just as fast? Do you mean to say that the Java->bytecode conversion is fast? Duh, it barely does anything. Go compilation generates machine code, you can't compare it to bytecode generation. Are Java AOT compilation times just as fast as Go? | | |
| ▲ | gf000 3 days ago | parent [-] | | > Duh, it barely does anything. Go compilation generates machine code, you can't compare it to bytecode generation Why not? Machine code is not all that special - C++ and Rust is slow due to optimizations, not for machine code as target itself. Go "barely does anything", just spits out machine code almost as is. Java AOT via GraalVM's native image is quite slow, but it has a different way of working (doing all the Java class loading and initialization and "baking" that into the native image). |
|
| |
| ▲ | const_cast 3 days ago | parent | prev | next [-] | | But go tooling is bad. Like, really really bad. Sure it's good compared to like... C++. Is go actually competing with C++? From where I'm standing, no. But compared to what you might actually use Go for... The tooling is bad. PHP has better tooling, dotnet has better tooling, Java has better tooling. | | |
| ▲ | ponow 3 days ago | parent | next [-] | | Go was a response, in part, to C++, if I recall how it was described when it was introduced. That doesn't seem to be how it ended it out. Maybe it was that "systems programming language" means something different for different people. | |
| ▲ | tom_m 3 days ago | parent | prev | next [-] | | Go tooling is among the best out there. You ever see npm? | |
| ▲ | melodyogonna 2 days ago | parent | prev [-] | | Go tooling is fine. | | |
| ▲ | const_cast a day ago | parent [-] | | Again, compared to languages that you probably shouldn't be contorting to make a web backend in, sure. Its better than C++ and JS (npm). Compared to incumbents like dotnet and PHP? Uhh, no. The tooling is very far behind and cumbersome in comparison. |
|
| |
| ▲ | Filligree 3 days ago | parent | prev [-] | | Unfortunately the lack of abstraction and simple type system in Go makes it far _slower_ for me to code than e.g. Rust. |
|
|
| |
| ▲ | rollcat 3 days ago | parent | prev [-] | | That's a bit unfair to the modern compilers - there's a lot more standards to adhere to, more (micro)architectures, frontends need to plug into IRs into optimisers into codegen, etc. Some of it is self-inflicted: do you need yet-another 0.01% optimisation? At the cost of maintainability, or even correctness? (Hello, UB.) But most of it is just computers evolving. But those are not rules. If you're doing stuff for fun, check out QBE <https://c9x.me/compile/> or Plan 9 C <https://plan9.io/sys/doc/comp.html> (which Go was derived from!) | | |
| ▲ | pjmlp 3 days ago | parent | next [-] | | Indeed, and thankfully there exist languages like D and Delphi to prove as being a modern compiler and fast compilation times are still possible 40 years later. | |
| ▲ | bsder 3 days ago | parent | prev [-] | | > That's a bit unfair to the modern compilers It's really not. Proebsting's Law applies. Given that, compilers/languages should be optimized for programmer productivity first and code speed second. |
|
| |
| ▲ | nine_k 3 days ago | parent | prev | next [-] | | If you want a nice modern compiled language, try Kotlin. It's not ideal, but it's very ergonomic and has very reasonable compile times (to JVM, I did not play with native compilation). People also praise Nim for being nice towards the developer, but I don't have any first-hand experience with it. | | |
| ▲ | lisbbb 3 days ago | parent [-] | | I have only used Kotlin on the JVM. You're saying there's a way to avoid the JVM and build binaries with it? Gotta go look that up. The problem with Kotlin is not the language but finding jobs using it can be spotty. "Kotlin specialist" isn't really a thing at all. You can find more Golang and Python jobs than Kotlin. |
| |
| ▲ | lisbbb 3 days ago | parent | prev [-] | | But it's not--Go is a thoroughly modern language, minus a few things as noted in this discussion. But it's very and I've written quite a few APIs for corporate clients using it and they are doing great. |
|
|
| ▲ | kace91 4 days ago | parent | prev | next [-] |
| My feeling is that in terms of developer ergonomics, it nailed the “very opinionated, very standard, one way of doing things” part. It is a joy to work on a large microservices architecture and not have a different style on each repo, or avoiding formatting discussions because it is included. The issue is that it was a bit outdated in the choice of _which_ things to choose as the one Go way. People expect a map/filter method rather than a loop with off by one risks, a type system with the smartness of typescript (if less featured and more heavily enforced), error handling is annoying, and so on. I get that it’s tough to implement some of those features without opening the way to a lot of “creativity” in the bad sense. But I feel like go is sometimes a hard sell for this reason, for young devs whose mother language is JavaScript and not C. |
| |
| ▲ | dkarl 3 days ago | parent | next [-] | | > The issue is that it was a bit outdated in the choice of _which_ things to choose as the one Go way I agree with this. I feel like Go was a very smart choice to create a new language to be easy and practical and have great tooling, and not to be experimental or super ambitious in any particular direction, only trusting established programming patterns. It's just weird that they missed some things that had been pretty well hashed out by 2009. Map/filter/etc. are a perfect example. I remember around 2000 the average programmer thought map and filter were pointlessly weird and exotic. Why not use a for loop like a normal human? Ten years later the average programmer was like, for loops are hard to read and are perfect hiding places for bugs, I can't believe we used to use them even for simple things like map, filter, and foreach. By 2010, even Java had decided that it needed to add its "stream API" and lambda functions, because no matter how awful they looked when bolted onto Java, it was still an improvement in clarity and simplicity. Somehow Go missed this step forward the industry had taken and decided to double down on "for." Go's different flavors of for are a significant improvement over the C/C++/Java for loop, but I think it would have been more in line with the conservative, pragmatic philosophy of Go to adopt the proven solution that the industry was converging on. | | |
| ▲ | throwaway920102 3 days ago | parent [-] | | Go Generics provides all of this. Prior to generics, you could have filter, map, reduce etc but you needed to implement them yourself once in a library/pkg and do it for each type. After Go added generics in version 1.18, you can just import someone else's generic implementations of whatever of these functions you want and use them all throughout your code and never think about it. It's no longer a problem. | | |
| ▲ | dkarl 3 days ago | parent [-] | | The language might permit it now, but it isn't designed for it. I think if the Go designers had intended for map, filter, et al to replace most for loops, they would have designed a more concise syntax for anonymous functions. Something more along the lines of: colors := items.Filter(_.age > 20).Map(_.color)
Instead of colors := items.Filter(func(x Item){ return x.age > 20 }).Map(func(x Item){ return x.color })
which as best as I can tell is how you'd express the same thing in Go if you had a container type with Map and Filter defined. |
|
| |
| ▲ | j1elo 4 days ago | parent | prev | next [-] | | > People expect a map/filter method Do they? After too many functional battles I started practicing what I'm jokingly calling "Debugging-Driven Development" and just like TDD keeps the design decisions in mind to allow for testability from the get-go, this makes me write code that will be trivially easy to debug (specially printf-guided debugging and step-by-step execution debugging) Like, adding a printf in the middle of a for loop, without even needing to understand the logic of the loop. Just make a new line and write a printf. I grew tired of all those tight chains of code that iterate beautifully but later when in a hurry at 3am on a Sunday are hell to decompose and debug. | | |
| ▲ | kace91 4 days ago | parent | next [-] | | I'm not a hard defender of functional programming in general, mind you. It's just that a ridiculous amount of steps in real world problems can be summarised as 'reshape this data', 'give me a subset of this set', or 'aggregate this data by this field'. Loops are, IMO, very bad at expressing those common concepts briefly and clearly. They take a lot of screen space, usually accesory variables, and it isn't immediately clear from just seing a for block what you're about to do - "I'm about to iterate" isn't useful information to me as a reader, are you transforming data, selecting it, aggregating it?. The consequence is that you usually end up with tons of lines like userIds = getIdsfromUsers(users); where the function is just burying a loop. Compare to: userIds = users.pluck('id') and you save the buried utility function somewhere else. | |
| ▲ | tuetuopay 3 days ago | parent | prev | next [-] | | Rust has `.inspect()` for iterators, which achieves your printf debugging needs. Granted, it's a bit harder for an actual debugger, but support's quite good for now. | |
| ▲ | williamdclt 4 days ago | parent | prev | next [-] | | I'll agree that explicit loops are easier to debug, but that comes at the cost of being harder to write _and_ read (need to keep state in my head) _and_ being more bug-prone (because mutability). I think it's a bad trade-off, most languages out there are moving away from it | | |
| ▲ | nasretdinov 4 days ago | parent [-] | | There's actually one more interesting plus for the for loops that's not quite obvious in the beginning: the for-loops allow to do perform a single memory pass instead of multiple. If you're processing a large enough list it does make a significant difference because memory accesses are relatively expensive (the difference is not insignificant, the loop can be made e.g. 10x more performant by optimising memory accesses alone). So for a large loop the code like for i, value := source {
result[i] = value * 2 + 1
} Would be 2x faster than a loop like for i, value := source {
intermediate[i] = value * 2
} for i, value := intermediate {
result[i] = value + 1
} | | |
| ▲ | tuetuopay 3 days ago | parent | next [-] | | Depending on your iterator implementation (or, lackthere of), the functional boils down to your first example. For example, Rust iterators are lazily evaluated with early-exits (when filtering data), thus it's your first form but as optimized as possible. OTOH python's map/filter/etc may very well return a full list each time, like with your intermediate. [EDIT] python returns generators, so it's sane. I would say that any sane language allowing functional-style data manipulation will have them as fast as manual for-loops. (that's why Rust bugs you with .iter()/.collect()) | | | |
| ▲ | kace91 3 days ago | parent | prev [-] | | This is a very valid point. Loops also let you play with the iteration itself for performance, deciding to skip n steps if a condition is met for example. I always encounter these upsides once every few years when preparing leetcode interviews, where this kind of optimization is needed for achieving acceptable results. In daily life, however, most of these chunks of data to transform fall in one of these categories: - small size, where readability and maintainability matters much more than performance - living in a db, and being filtered/reshaped by the query rather than code - being chunked for atomic processing in a queue or similar (usual when importing a big chunk of data). - the operation itself is a standard algorithm that you just consume from a standard library that handless the loop internally. Much like trees and recursion, most of us don’t flex that muscle often. Your mileage might vary depending of domain of course. | | |
| ▲ | empath75 3 days ago | parent [-] | | There's also that rust does a _lot_ of compiler optimizations on map/filter/reduce and it's trivially parallelizable in many cases. |
|
|
| |
| ▲ | lenkite 4 days ago | parent | prev | next [-] | | This depends on the language and IDE. Intellij Java debugger is excellent at stream debugging. | |
| ▲ | const_cast 3 days ago | parent | prev [-] | | Just use a real debugger. You can step into closures and stuff. I assume, anyway. Maybe the Go debugger is kind of shitty, I don't know. But in PHP with xdebug you just use all the fancy array_* methods and then step through your closures or callables with the debugger. |
| |
| ▲ | javier2 3 days ago | parent | prev [-] | | The lack of stack traces in Go is diabolical for all the effort we have to out in by manually passing every error |
|
|
| ▲ | BugsJustFindMe 4 days ago | parent | prev | next [-] |
| > Go was designed by some old-school folks that maybe stuck a bit too hard to their principles, losing sight of the practical conveniences. It feels often like the two principles they stuck/stick to are "what makes writing the compiler easier" and "what makes compilation fast". And those are good goals, but they're only barely developer-oriented. |
| |
| ▲ | xg15 4 days ago | parent | next [-] | | Not sure it was only that. I remember a lot of "we're not Java" in the discussions around it. I always had the feeling, they were rejecting certain ideas like exceptions and generics more out of principle, than any practical analysis. Like, yes, those ideas have frequently been driven too far and have led to their own pain points. But people also seem to frequently rediscover that removing them entirety will lead to pain, too. | | |
| ▲ | nine_k 3 days ago | parent [-] | | Ian Lance Taylor, a big proponent of generics, wrote a lot about the difficulties of adding generics to Golang. I bet the initial team just had to cut the scope and produce a working language, as simple as possible while still practically useful. Easy concurrency was the goal, so they basically took mostl of Modula-2 plus ideas form Oberon (and elsewhere), removed all the "fluff" (like arrays indexable by enumeration types, etc), added GC, and that was plenty enough. | | |
| ▲ | xg15 3 days ago | parent [-] | | I feel especially with generics though, there is a sort of loop that many languages fall into. It goes something like this: (1) "Generics are too complicated and academical and in the real world we only need them for a small number of well-known tasks anyway, so let's just leave them out!" (2) The amount of code that does need generics but now has to work around the lack of them piles up, leading to an explosion of different libraries, design patterns, etc, that all try to partially recreate them in their own way. (3) The language designers finally cave and introduce some kind of generics support in a later version of the language. However, at this point, they have to deal with all the "legacy" code that is not generics-aware and with runtime environments that aren't either. It also somehow has to play nice with all the ad-hoc solutions that are still present. So the new implementation has to deal with a myriad of special cases and tradeoffs that wouldn't be there in the first if it had been included in the language from the beginning. (4) All the tradeoffs give the feature a reputation of needless complexity and frustrating limitations and/or footguns, prompting the next language designer to wonder if they should include them at all. Go to (1) ... |
|
| |
| ▲ | bojo 3 days ago | parent | prev | next [-] | | I recall that one of the primary reasons they built Go was because of the half-day compile times Google's C++ code was reaching. | |
| ▲ | zhengyi13 3 days ago | parent | prev | next [-] | | I am reminded when I read "barely developer oriented" that this comes from Google, who run compute and compilers at Ludicrous Scale. It doesn't seem strange that they might optimize (at least in part) for compiler speed and simplicity. | |
| ▲ | sureshv 3 days ago | parent | prev | next [-] | | What makes compilation fast is a good goal at places with large code bases and build times. Maybe makes less sense in smaller startups with a few 100k LOC. | | |
| ▲ | BugsJustFindMe 10 hours ago | parent [-] | | This describes the classic scenario of "You don't have google's problems, so maybe stop trying to hype google's toys". Protobuf feels similarly developer-hostile vs alternatives, and I remember a lot of similar sentiment around using it. It's funny that people don't seem to learn the lesson. |
| |
| ▲ | tom_m 3 days ago | parent | prev [-] | | Ah well you know, the kids want new stuff. They don't actually care about getting work done. |
|
|
| ▲ | bwfan123 3 days ago | parent | prev | next [-] |
| > Concurrency is tricky The go language and its runtime is the only system I know that is able to handle concurrency with multicore cpus seamlessly within the language, using the CSP-like (goroutine/channel) formalism which is easy to reason with. Python is a mess with the gil and async libraries that are hard to reason with. C,C++,Java etc need external libraries to implement threading which cant be reasoned with in the context of the language itself. So, go is a perfect fit for the http server (or service) usecase and in my experience there is no parallel. |
| |
| ▲ | ashton314 3 days ago | parent | next [-] | | > So, go is a perfect fit for the http server (or service) usecase and in my experience there is no parallel. Elixir handling 2 million websocket connections on a single machine back in 2015 would like to have a word.[1] This is largely thanks to the Erlang runtime it sits atop. Having written some tricky Go (I implemented Raft for a class) and a lot of Elixir (professional development), it is my experience that Go's concurrency model works for a few cases but largely sucks in others and is way easier to write footguns in Go than it ought to be. [1]: https://phoenixframework.org/blog/the-road-to-2-million-webs... | | |
| ▲ | Fire-Dragon-DoL 3 days ago | parent [-] | | I worked in both Elixir and Go. I still think Elixir is best for concurrency. I recently realized that there is no easy way to "bubble up a goroutine error", and I wrote some code to make sure that was possible, and that's when I realize, as usual, that I'm rewriting part of the OTP library. The whole supervisor mechanism is so valuable for concurrency. |
| |
| ▲ | Jtsummers 3 days ago | parent | prev | next [-] | | > Java etc need external libraries to implement threading Java does not need external libraries to implement threading, it's baked into the language and its standard libraries. | |
| ▲ | dehrmann 3 days ago | parent | prev | next [-] | | > Java etc need external libraries to implement threading which cant be reasoned with in the context of the language itself. What do you mean by this for Java? The library is the runtime that ships with Java, and while they're OS threads under the hood, the abstraction isn't all that leaky, and it doesn't feel like they're actually outside the JVM. Working with them can be a bit clunky, though. | | |
| ▲ | gf000 3 days ago | parent | next [-] | | Also, Java is one of the only languages with actually decent concurrent data structures right out of the box. | |
| ▲ | dboreham 3 days ago | parent | prev [-] | | I think parent means they're (mostly) not supported via keywords.
But you can use Kotlin and get that. | | |
| |
| ▲ | stouset 3 days ago | parent | prev | next [-] | | With all due respect, there are many languages in popular use that can do this, in many cases better than golang. I believe it’s the only system you know. But it’s far from the only one. | | |
| ▲ | antonchekhov 3 days ago | parent | next [-] | | > there are many languages in popular use that can do this, in many cases better than golang I'd love to see a list of these, with any references you can provide. | | |
| ▲ | Jtsummers 3 days ago | parent | next [-] | | Erlang, Elixir, Ada, plenty of others. Erlang and Ada predate Go by several decades, too. You wanted sources, here's the chapter on tasks and synchronization in the Ada LRM: http://www.ada-auth.org/standards/22rm/html/RM-9.html For Erlang and Elixir, concurrent programming is pretty much their thing so grab any book or tutorial on them and you'll be introduced to how they handle it. | |
| ▲ | jose_zap 3 days ago | parent | prev [-] | | Haskell would be one of them. It features transactional memory, which frees the programmer from having to think about explicitly locking. |
| |
| ▲ | ibic 3 days ago | parent | prev | next [-] | | Please elaborate or give some examples to back your claim? | |
| ▲ | dismalaf 3 days ago | parent | prev [-] | | There's not that many. C/C++ and Rust all map to OS threads and don't have CSP type concurrency built in. In Go's category, there's Java, Haskell, OCaml, Julia, Nim, Crystal, Pony... Dynamic languages are more likely to have green threads but aren't Go replacements. | | |
| ▲ | Jtsummers 3 days ago | parent | next [-] | | > There's not that many. You list three that don't, and then you go on to list seven languages that do. Yes, not many languages support concurrency like Go does... | | |
| ▲ | dismalaf 3 days ago | parent [-] | | And of those seven, how many are mainstream? A single one... So it's really Go vs. Java, or you can take a performance hit and use Erlang (valid choice for some tasks but not all), or take a chance on a novel paradigm/unsupported language. | | |
| ▲ | Jtsummers 3 days ago | parent | next [-] | | If you want mainstream, Java and C# are mainstream and both are used much more than Go. Clojure isn't too niche, though not as popular as Go, and supports concurrency out of the box at least as well as Go. Ada is still used widely within its niches and has better concurrency than Go baked in since 1983. And then, yes, Erlang and Elixir to add to that list. That's 6 languages, a non-exhaustive list of them, that are either properly mainstream and more popular than Go or at least well-known and easy to obtain and get started with. All of which have concurrency baked in and well-supported (unlike, say, C). EDIT: And one more thing, all but Elixir are older than Go, though Clojure only slightly. So prior art was there to learn from. | |
| ▲ | 3 days ago | parent | prev [-] | | [deleted] |
|
| |
| ▲ | jen20 3 days ago | parent | prev [-] | | Erlang (or Elixir) are absolutely Go replacements for the types of software where CSP is likely important. Source: spent the last few weeks at work replacing a Go program with an Elixir one instead. I'd use Go again (without question) but it is not a panacea. It should be the default choice for CLI utilities and many servers, but the notion that it is the only usable language with something approximating CSP is idiotic. |
|
| |
| ▲ | hombre_fatal 3 days ago | parent | prev | next [-] | | > using the CSP-like (goroutine/channel) formalism which is easy to reason with I thought it was a seldom mentioned fact in Go that CSP systems are impossible to reason about outside of toy projects so everyone uses mutexes and such for systemic coordination. I'm not sure I've even seen channels in a production application used for anything more than stopping a goroutine, collecting workgroup results, or something equally localized. | | |
| ▲ | kbolino 3 days ago | parent [-] | | There's also atomic operations (sync/atomic) and higher-level abstractions built on atomics and/or mutexes (sempahores, sync.Once, sync.WaitGroup/errgroup.Group, etc.). I've used these and seen them used by others. But yeah, the CSP model is mostly dead. I think the language authors' insistence that goroutines should not be addressable or even preemptible from user code makes this inevitable. Practical Go concurrency owes more to its green threads and colorless functions than its channels. | | |
| ▲ | diarrhea 3 days ago | parent [-] | | > colorless Functions are colored: those taking context.Context and those who don't. But I agree, this is very faint coloring compared to async implementations. One is free to context.Background() liberally. |
|
| |
| ▲ | hintymad 3 days ago | parent | prev | next [-] | | Unless we consider JDK as external library. Speaking of library, Java's concurrency containers are truly powerful yet can be safely used by so many engineers. I don't think Go's ecosystem is even close. | |
| ▲ | gf000 3 days ago | parent | prev | next [-] | | Go is such a good fit for multi-core, especially that it is not even memory safe under data races.. | | |
| ▲ | kbolino 3 days ago | parent [-] | | It is rare to encounter this in practice, and it does get picked up by the race detector (which you have to consciously enable). But the language designers chose not to address it, so I think it's a valid criticism. [1] Once you know about it, though, it's easy to avoid. I do think, especially given that the CSP features of Go are downplayed nowadays, this should be addressed more prominently in the docs, with the more realistic solutions presented (atomics, mutexes). It could also potentially be addressed using 128-bit atomics, at least for strings and interfaces (whereas slices are too big, taking up 3 words). The idea of adding general 128-bit atomic support is on their radar [2] and there already exists a package for it [3], but I don't think strings or interfaces meet the alignment requirements. [1]: https://research.swtch.com/gorace [2]: https://github.com/golang/go/issues/61236 [3]: https://pkg.go.dev/github.com/CAFxX/atomic128 |
| |
| ▲ | asa400 3 days ago | parent | prev | next [-] | | Erlang. | |
| ▲ | FuriouslyAdrift 3 days ago | parent | prev | next [-] | | Erlang? | |
| ▲ | dcrazy 3 days ago | parent | prev | next [-] | | Swift? JavaScript? | | |
| ▲ | nothrabannosir 3 days ago | parent [-] | | JavaScript? How, web workers? JavaScript is M:1 threaded. You can’t use multiple cores without what basically amounts to user space ipc | | |
| ▲ | mhink 3 days ago | parent | next [-] | | Not to dispute too strongly (since I haven't used this functionality myself), but Node.js does have support for true multithreading since v12: https://nodejs.org/dist/latest/docs/api/worker_threads.html. I'm not sure what you mean by "M:1 threaded" but I'm legitimately curious to understand more here, if you're willing to give more details. There are also runtimes like e.g. Hermes (used primarily by React Native), there's support for separating operations between the graphics thread and other threads. All that being said, I won't dispute OP's point about "handling concurrency [...] within the language"- multithreading and concurrency are baked into the Golang language in a more fundamental way than Javascript. But it's certainly worth pointing out that at least several of the major runtimes are capable of multithreading, out of the box. | | |
| ▲ | nothrabannosir 3 days ago | parent | next [-] | | Yeah those are workers which require manual admin of memory shared / passed memory: > Within a worker thread, worker.getEnvironmentData() returns a clone of data passed to the spawning thread's worker.setEnvironmentData(). Every new Worker receives its own copy of the environment data automatically. M:1 threaded means that the user space threads are mapped onto a single kernel thread. Go is M:N threaded: goroutines can be arbitrarily scheduled across various underlying OS threads. Its primitives (goroutines and channels) make both concurrency and parallelism notably simpler than most languages. > But it's certainly worth pointing out that at least several of the major runtimes are capable of multithreading, out of the box. I’d personally disagree in this context. Almost every language has pthread-style cro-magnon concurrency primitives. The context for this thread is precisely how go differs from regular threading interfaces. Quoting gp: > The go language and its runtime is the only system I know that is able to handle concurrency with multicore cpus seamlessly within the language, using the CSP-like (goroutine/channel) formalism which is easy to reason with. Yes other languages have threading, but in go both concurrency and parallelism are easier than most. (But not erlang :) ) | |
| ▲ | odo1242 3 days ago | parent | prev | next [-] | | I had to look M:1 threading up too - it's this: https://en.wikipedia.org/wiki/Thread_(computing)#M:1_(user-l... Basically OP was saying that JavaScript can run multiple tasks concurrently, but with no parallelism since all tasks map to 1 OS thread. | | | |
| ▲ | tom_m 3 days ago | parent | prev [-] | | Even the node author came out and said the concurrency design there was wrong and switched to golang. Libuv is cool and all, but doesn't handle everything and you still have a bottleneck, poor isolation, and the single threaded event loop to deal with. Back pressure in node becomes a real thing and the whole thing becomes very painful and obvious at scale. Granted, many people don't ever need to handle that kind of throughput. It depends on the app and the load put on to it. So many people don't realize. Which is fine! If it works it works. But if you do fall into the need of concurrency, yea, you probably don't want to be using node - even the newer versions. You certainly could do worse than golang. It's good we have some choices out there. The other thing I always say is the choice in languages and technology is not for one person. It's for the software and team at hand. I often choose languages, frameworks, and tools specifically because of the team that's charged with building and maintaining. If you can make them successful because a language gives them type safety or memory safety that rust offers or a good tool chain, whatever it is that the team needs - that's really good. In fact, it could well be the difference between a successful business and an unsuccessful one. No one really cares how magical the software is if the company goes under and no one uses the software. |
| |
| ▲ | 3 days ago | parent | prev [-] | | [deleted] |
|
| |
| ▲ | peterashford 2 days ago | parent | prev | next [-] | | "Java etc need external libraries to implement threading" Ah, what???? | |
| ▲ | mervz 3 days ago | parent | prev [-] | | This is a diabolical take |
|
|
| ▲ | traceroute66 4 days ago | parent | prev | next [-] |
| > Just all-around a trusty tool in the belt I agree. The Go std-lib is fantastic. Also no dependency-hell with Go, unlike with Python. Just ship an oven-ready binary. And what's the alternative ? Java ? Licensing sagas requiring the use of divergent forks. Plus Go is easier to work with, perhaps especially for server-side deployments. Zig ? Rust ? Complex learning curve. And having to choose e.g. Rust crates re-introduces dependency hell and the potential for supply-chain attacks. |
| |
| ▲ | gf000 4 days ago | parent | next [-] | | > Java ? Licensing sagas requiring the use of divergent forks. Plus Go is easier to work with, perhaps especially for server-side deployments Yeah, these are sagas only, because there is basically one, single, completely free implementation anyone uses on the server-side and it's OpenJDK, which was made 100% open-source and the reference implementation by Oracle. Basically all of Corretto, AdoptOpenJDK, etc are just builds of the exact same repository. People bringing this whole license topic up can't be taken seriously, it's like saying that Linux is proprietary because you can pay for support at Red Hat.. | | |
| ▲ | traceroute66 4 days ago | parent | next [-] | | > People bringing this whole license topic up can't be taken seriously So you mean all those universities and other places that have been forced to spend $$$ on licenses under the new regime also can't be taken seriously ? Are you saying none of them took advice and had nobody on staff to tell them OpenJDK exists ? Regarding your Linux comment, some of us are old enough to remember the SCO saga. Sadly Oracle have deeper pockets to pay more lawyers than SCO ever did .... | | |
| ▲ | piva00 4 days ago | parent | next [-] | | > So you mean all those universities and other places that have been forced to spend $$$ on licenses under the new regime also can't be taken seriously ? Are you saying none of them took advice and had nobody on staff to tell them OpenJDK exists ? This info is actually quite surprising to me, never heard of it since everywhere I know switched to OpenJDK-based alternatives from the get-go. There was no reason to keep on the Oracle one after the licencing shenanigans they tried to play. Why do these places kept the Oracle JDK and ended up paying for it? OpenJDK was a drop-in replacement, nothing of value is lost by switching... | | |
| ▲ | traceroute66 4 days ago | parent [-] | | TL;DR: Its impossible to know if anyone on campus has downloaded Oracle Java....Oracle monitors downloads and sends in the auditors... See link/quote in my earlier reply above. | | |
| ▲ | wing-_-nuts 4 days ago | parent [-] | | The licensing thing is such FUD man. Oracle being a terrible company is in no way a decent argument that Java should not be used. | | |
| ▲ | lolc 3 days ago | parent | next [-] | | > Oracle being a terrible company is in no way a decent argument that Java should not be used. Weird, to me that is a strong argument. Choose your stewards. | |
| ▲ | 3 days ago | parent | prev [-] | | [deleted] |
|
|
| |
| ▲ | gf000 4 days ago | parent | prev [-] | | I have made a bunch of claims, that are objectively true. From there, basic logical inference says that you can completely freely use Java. Anything else is irrelevant. I don't know what/which university you talk about, but I'm sure they were also "forced to pay $$$" for their water bills and whatnot. If they decided to go with paid support, then.. you have to pay for it. In exchange you can a) point your finger at a third-party if something goes wrong (which governments love doing/often legally necessary) b) get actual live support on Christmas Eve if needed. | | |
| ▲ | traceroute66 4 days ago | parent [-] | | TL;DR: Its impossible to know if anyone on campus has downloaded Oracle Java.... Quote from this article:[1] *He told The Register that Oracle is "putting specific Java sales teams in country, and then identifying those companies that appear to be downloading and... then going in and requesting to [do] audits. That recipe appears to be playing out truly globally at this point."*
[1] https://www.theregister.com/2025/06/13/jisc_java_oracle/ | | |
| ▲ | gf000 4 days ago | parent | next [-] | | That's also true of torrented PhotoShop, Microsoft Office, etc.. Also, as another topic, Oracle is doing audits specifically because their software doesn't phone home to check licenses and stuff like that - which is a crucial requirement for their intended target demographics, big government organizations, safety critical systems, etc. A whole country's healthcare system, or a nuclear power base can't just stop because someone forgot to pay the bill. So instead Oracle just visits companies that have a license with them, and checks what is being used to determine if it's in accord with the existing contract. And yeah, from this respect I also heard of a couple of stories where a company was not using the software as the letter of the contract, e.g. accidentally enabling this or that, and at the audit the Oracle salesman said that they will ignore the mistake if they subscribe to this larger package, which most manager will gladly accept as they can avoid the blame, which is questionable business practice, but still doesn't have anything to do with OpenJDK.. | |
| ▲ | josefx 4 days ago | parent | prev [-] | | > Quote from this article:[1] The article tries very hard to draw a connection between the licensing costs for the universities and Oracle auditing random java downloads, but nobody actually says that this is what happened. The waiver of historic fees goes back to the last licensing change where Oracle changed how licensing fees would be calculated. So it seems reasonable that Oracle went after them because they were paying customers that failed to pay the inflated fees. |
|
|
| |
| ▲ | pjmlp 4 days ago | parent | prev [-] | | There are other JVMs that do not descend from OpenJDK, but in general your point stands. | | |
| ▲ | gf000 4 days ago | parent [-] | | Yeah I know, but people have trouble understanding the absolutely trivial licensing around OpenJDK, let's not bring up alternative implementations (which actually makes the whole platform an even better target from a longevity perspective! There isn't many languages that have a standard with multiple, completely independent impls). |
|
| |
| ▲ | brabel 3 days ago | parent | prev | next [-] | | You forgot D. In a world where D exists, it's hard to understand why Go needed to be created. Every critique in this post is not an issue in D. If the effort Google put into Go had gone on making D better, I think D today would be the best language you could use. But as it is, D has had very little investment (by that I mean actual developer time spent on making it better, cleaning it up, writing tools) and it shows. | | |
| ▲ | maleldil 3 days ago | parent [-] | | I don't think the languages are comparable. Go tries to stay simple (whatever that means), while D is a kitchen-sink language. |
| |
| ▲ | tempay 4 days ago | parent | prev | next [-] | | > Rust crates re-introduces dependency hell and the potential for supply-chain attacks. I’m only a casual user of both but how are rust crates meaningfully different from go’s dependency management? | | |
| ▲ | jaas 4 days ago | parent | next [-] | | Go has a big, high quality standard library with most of what one might need. Means you have to bring in and manage (and trust) far fewer third party dependencies, and you can work faster because you’re not spending a bunch of time figuring out what the crate of the week is for basic functionality. | | |
| ▲ | zozbot234 4 days ago | parent [-] | | Rust intentionally chooses to have a small standard library to avoid the "dead batteries" problem. But the Rust community also maintains lists of "blessed" crates to try and cope with the issue of having to trust third-party software components of unknown quality. | | |
| ▲ | jen20 3 days ago | parent | next [-] | | Different trade offs, both are fine. The downside of a small stdlib is the proliferation of options, and you suddenly discover(ed?, it's been a minute) that your async package written for Tokio won't work on async-std and so forth. This has often been the case in Go too - until `log/slog` existed, lots of people chose a structured logger and made it part of their API, forcing it on everyone else. | | |
| ▲ | surajrmal 2 days ago | parent [-] | | It would be nice if folks linked crates up into ecosystems thst shared maintainers and guidelines. We don't need everyone using the same stuff, but I'd rather prefer to get 10 different dependences rather than 30. In c++ this plays out in libraries like absl, folly, boost and others. Fewer larger dependencies that bring in a mix of functionality. |
| |
| ▲ | traceroute66 4 days ago | parent | prev [-] | | > Rust intentionally chooses to have a small standard library to avoid the "dead batteries" problem. There is a difference between "small" and Rust's which is for all intents and purposes, non-existent. I mean, in 2025, not having crypto in stdlib when every man and his dog is using crypto ? Or http when every man and his dog are calling REST APIs ? As the other person who replied to you said. Go just allows you to hit the ground running and get on with it. Having to navigate the world of crates, unofficially "blessed" or not is just a bit of a re-inventing the wheel scenario really.... P.S. The Go stdlib is also well maintained, so I don't really buy the specific "dead batteries" claim either. | | |
| ▲ | tremon 4 days ago | parent | next [-] | | I think having http in the standard library is a perfect example of the dead batteries problem: should the stdlib http also support QUIC and/or websockets? If you choose to include it, you've made stdlib include support for very specific use cases. If you choose not to include it, should the quic crate then extend or subsume the stdlib http implementation? If you choose subsume, you've created a dead battery. If you choose extend, you've created a maintenance nightmare by introducing a dependency between stdlib and an external crate. | |
| ▲ | deagle50 4 days ago | parent | prev | next [-] | | > I mean, in 2025, not having crypto in stdlib when every man and his dog is using crypto ? Or http when every man and his dog are calling REST APIs ? I'm not and I'm glad the core team doesn't have to maintain an http server and can spend time on the low level features I chose Rust for. | |
| ▲ | tuetuopay 3 days ago | parent | prev | next [-] | | Sorry but for most programming tasks I prefer having actual data containers with features than an HTTP library: Set, Tree, etc types. Those are fundamental CS building blocks yet are absent from the Go standard library. (well, they were added pretty recently, still nowhere near as featureful than std::collection in Rust). Also, as mentioned by another comment, an HTTP or crypto library can become obsolete _fast_. What about HTTP3? What about post-quantum crypto? What about security fixes? The stdlib is tied to the language version, thus to a language release. Having such code independant allows is to evolve much faster, be leaner, and be more composable. So yes, the library is well maintained, but it's tied to the Go version. Also, it enables breaking API changes if absolutely needed. I can name two precendents: - in rust, time APIs in chrono had to be changed a few times, and the Rust maintainers were thankful it was not part of the stdlib, as it allowed massive changes - otoh, in Go, it was found out that net.Ip has an absolutely atrocious design (it's just an alias for []byte). Tailscale wrote a replacement that's now in a subpackage in net, but the old net.Ip is set in stone. (https://tailscale.com/blog/netaddr-new-ip-type-for-go) | | |
| ▲ | Mawr 3 days ago | parent [-] | | > Set, Tree, etc types. Those are fundamental CS building blocks And if you're engaging in CS then Go is probably the last language you should be using. If however, what you're interested in doing is programming, the fundamental data structures there are arrays and hashmaps, which Go has built-in. Everything else is niche. > Also, as mentioned by another comment, an HTTP or crypto library can become obsolete _fast_. What about HTTP3? What about post-quantum crypto? What about security fixes? The stdlib is tied to the language version, thus to a language release. Having such code independant allows is to evolve much faster, be leaner, and be more composable. So yes, the library is well maintained, but it's tied to the Go version. The entire point is to have a well supported crypto library. Which Go does and it's always kept up to date. Including security fixes. As for post-quantum: https://words.filippo.io/mlkem768/ > - otoh, in Go, it was found out that net.Ip has an absolutely atrocious design (it's just an alias for []byte). Tailscale wrote a replacement that's now in a subpackage in net, but the old net.Ip is set in stone. (https://tailscale.com/blog/netaddr-new-ip-type-for-go) Yes, and? This seems to me to be the perfect way to handle things - at all times there is a blessed high-quality library to use. As warts of its design get found out over time, a new version is worked on and released once every ~10 years. A total mess of barely-supported libraries that the userbase is split over is just that - a mess. |
| |
| ▲ | arlort 3 days ago | parent | prev | next [-] | | The go stdlib is well maintained and featureful because Google is very invested in it being both of those things for the use cases That works well for go and Google but I'm not sure how easily that'd be to replicate with rust or others | |
| ▲ | duped 3 days ago | parent | prev [-] | | Do you think C and C++ should have http or crypto in their standard libraries? | | |
|
|
| |
| ▲ | tzekid 4 days ago | parent | prev [-] | | I think it's because go's community sticks close to the standard library: e.g. iirc. Rust has multiple ways of handling Strings while Go has (to a big extent) only one (thanks to the GC) | | |
| ▲ | diarrhea 3 days ago | parent | next [-] | | > Rust has multiple ways of handling Strings No, none outside of stdlib anyway in the way you're probably thinking of. There are specialized constructs which live in third-party crates, such as rope implementations and stack-to-heap growable Strings, but those would have to exist as external modules in Go as well. | |
| ▲ | adastra22 3 days ago | parent | prev [-] | | What does String/OsSfeing have to do with garbage collection? |
|
| |
| ▲ | theshrike79 4 days ago | parent | prev | next [-] | | uv + the new way of adding the required packages in the comments is pretty good. you can go `uv run script.py` and it'll automatically fetch the libraries and run the script in a virtual environment. Still no match for Go though, shipping a single cross-compiled binary is a joy. And with a bit of trickery you can even bundle in your whole static website in it :) Works great when you're building business logic with a simple UI on top. | | |
| ▲ | richid 4 days ago | parent | next [-] | | I've been out of the Python game for a while but I'm not surprised there is yet another tool on the market to handle this. You really come to appreciate when these batteries are included with the language itself. That Go binary will _always_ run but that Python project won't build in a few years. | | |
| ▲ | pjmlp 4 days ago | parent [-] | | Unless it made use of CGO and has dynamic dependencies, always is a bit too much. | | |
| ▲ | mdaniel 3 days ago | parent [-] | | Or the import path was someone's blog domain that included a <meta> reference to the actual github repo (along with the tag, IIRC) where the source code really lives. Insanity | | |
| ▲ | pjmlp 3 days ago | parent [-] | | I never understood the mentality to have SCM urls as package imports directly on the source code. | | |
| ▲ | mdaniel 3 days ago | parent [-] | | Well, that's the problem I was highlighting - golang somehow decided to have the worst of both worlds: arbitrary domains in import paths and then putting the actual ref of the source code ... elsewhere import "gopkg.in/yaml.v3" // does *what* now?
curl https://gopkg.in/yaml.v3?go-get=1 | grep github
<meta name="go-source" content="gopkg.in/yaml.v3 _ https://github.com/go-yaml/yaml/tree/v3.0.1{/dir} https://github.com/go-yaml/yaml/blob/v3.0.1{/dir}/{file}#L{line}">
oh, ok :-/I would presume only a go.mod entry would specify whether it really is v3.0.0 or v3.0.1 Also, for future generations, don't use that package https://github.com/go-yaml/yaml#this-project-is-unmaintained |
|
|
|
| |
| ▲ | lenkite 4 days ago | parent | prev | next [-] | | uv is the new hotness now. Let us check back in 5 years... | |
| ▲ | traceroute66 4 days ago | parent | prev [-] | | > you can go `uv run script.py` and it'll automatically fetch the libraries and run the script in a virtual environment. Yeah, but you still have to install `uv` as a pre-requisite. And you still end up with a virtual environment full of dependency hell. And then of course we all remember that whole messy era when Python 2 transitioned to Python 3, and then deferred it, and deferred it again.... You make a fair point, of course it is technically possible to make it (slightly) "cleaner". But I'll still take the Go binary thanks. ;-) | | |
| ▲ | rsyring 3 days ago | parent [-] | | Installing uv is a requirement and incredibly easy. No, there is no dependency hell in the venv. Python 2 to 3: are you really still kicking that horse? It's dead...please move on. |
|
| |
| ▲ | morsecodist 3 days ago | parent | prev | next [-] | | This just makes it even more frustrating to me. Everything good about go is more about the tooling and ecosystem but the language itself is not very good. I wish this effort had been put into a better language. | | |
| ▲ | iTokio 3 days ago | parent | next [-] | | Go has transparent async io and a very nice M:N threading model that makes writing http servers using epoll very simple and efficient. The ergonomics for this use case are better than in any language I ever used. | | |
| ▲ | layer8 3 days ago | parent [-] | | Implementing HTTP servers isn’t exactly a common use case in software development, though. | | |
| ▲ | iTokio 2 days ago | parent [-] | | Sorry I didn’t mean implementing a raw http server like nginx, but just writing a backend. |
|
| |
| ▲ | p2detar 3 days ago | parent | prev [-] | | > I wish this effort had been put into a better language. But it is being put. Read newsletters like "The Go Blog", "Go Weekly". It's been improving constantly. Language-changes require lots of time to be done right, but the language is evolving. |
| |
| ▲ | 4 days ago | parent | prev | next [-] | | [deleted] | |
| ▲ | Luker88 3 days ago | parent | prev | next [-] | | > Rust crates re-introduces [...] potential for supply-chain attacks. I have absolutely no idea how go would solve this problem, and in fact I don't think it does at all. > The Go std-lib is fantastic. I have seen worse, but I would still not call it decent considering this is a fairly new language that could have done a lot more. I am going to ignore the incredible amount of asinine and downright wrong stuff in many of the most popular libraries (even the basic ones maintained by google) since you are talking only about the stdlib. On the top of my head I found inconsistent tagging management for structs (json defaults, omitzero vs omitempty), not even errors on tag typos, the reader/writer pattern that forces you to to write custom connectors between the two, bzip2 has a reader and no writer, the context linked list for K/V. Just look at the consistency of the interfaces in the "encoding" pkg and cry, the package `hash` should actually be `checksum`. Why does `strconv.Atoi`/ItoA still exist? Time.Add() vs Time.Sub()... It chock full of inconsistencies. It forces me to look at the documentation every single time I don't use something for more than a couple of days. No, the autocomplete with the 2-line documentation does not include the potential pitfalls that are explained at the top of the package only. And please don't get me started on the wrappers I had to write around stuff in the net library to make it a bit more consistent or just less plain wrong. net/url.Parse!!! I said don't make my start on this package! nil vs NoBody! ARGH! None of this is stuff at the language level (of which there is plenty to say). None of it is a dealbreaker per se, but it adds attrition and becomes death by a billion cuts. I don't even trust any parser written in go anymore, I always try to come up with corner cases to check how it reacts, and I am often surprised by most of them. Sure, there are worse languages and libraries. Still not something I would pick up in 2025 for a new project. | |
| ▲ | porridgeraisin 4 days ago | parent | prev [-] | | > std-lib Yes, My favourite is the `time` package. It's just so elegant how it's just a number under there, the nominal type system truly shines. And using it is a treat.
What do you mean I can do `+= 8*time.Hour` :D | | |
| ▲ | tux3 4 days ago | parent | next [-] | | Unfortunately it doesn't have error handling, so when you do += 8 hours and it fails, it won't return a Go error, it won't throw a Go exception, it just silently does the wrong thing (clamp the duration) and hope you don't notice... It's simplistic and that's nice for small tools or scripts, but at scale it becomes really brittle since none of the edge cases are handled | | |
| ▲ | arethuza 4 days ago | parent [-] | | When would that fail - if the resulting time is before the minimum time or after the maximum time? | | |
| ▲ | tux3 4 days ago | parent [-] | | I thankfully found out when writing unit tests instead of in production. In Go time.Time has a much higher range than time.Duration, so it's very easy to have an overflow when you take a time difference. But there's also no error returned in general when manipulating time.Duration, you have to remember to check carefully around each operation to know if it risks going out of range. Internally time.Duration is a single 64bit count, while time.Time is two more complicated 64bit fields plus a location | | |
|
| |
| ▲ | candiddevmike 4 days ago | parent | prev | next [-] | | The way Go parses time strings by default is insane though, even the maintainers regret it. It's a textbook example of being too clever. | | |
| ▲ | nkozyra 4 days ago | parent [-] | | By choosing default values instead of templatized values? Other than having to periodically remember what 0-padded milliseconds are or whatever this isn't a huge deal. | | |
| |
| ▲ | pansa2 4 days ago | parent | prev [-] | | As long as you don’t need to do `hours := 8` and `+= hours * time.Hour`. Incredibly the only way to get that multiplication to work is to cast `hours` to a `time.Duration`. In Go, `int * Duration = error`, but `Duration * Duration = Duration`! | | |
| ▲ | porridgeraisin 3 days ago | parent [-] | | That is consistent though. Constants take type based on context, so 8 * time.Hour has 8 as a time.Duration. If you have an int variable hours := 8, you have to cast it before multiplying. This is also true for simple int and float operations. f := 2.0
3 * f
is valid, but x := 3 would need float64(x)*f to be valid. Same is true for addition etc. | | |
|
|
|
|
| ▲ | theshrike79 4 days ago | parent | prev | next [-] |
| People tend to refer to the bit where Discord rewrote a bit of their stack in Rust because Go GC pauses were causing issues. The code was on the hot path of their central routing server handling Billions (with a B) messages in a second or something crazy like that. You're not building Discord, the GC will most likely never be even a blip in your metrics. The GC is just fine. |
| |
| ▲ | AtlasBarfed 3 days ago | parent [-] | | I get you can specifically write code that does not malloc, but I'm curious at scale if there are heap management / fragmentation and compression issues that are equivalent to GC pause issues. I don't have a lot of experience with the malloc languages at scale, but I do know that heat fragmentation and GC fragmentation are very similar problems. There are techniques in GC languages to avoid GC like arena allocation and stuff like that, generally considered non-idiomatic. |
|
|
| ▲ | apwell23 4 days ago | parent | prev | next [-] |
| > But yeah the whole error / nil situation still bothers me. I find myself wishing for Result[Ok, Err] and Optional[T] quite often. I got insta rejected in interview when i said this in response to interview panels question about 'thoughts about golang' . Like they said, 'interview is over' and showed me the (virtual) door. I was stunned lol. This was during peak golang mania . Not sure what happened to rancherlabs . |
| |
| ▲ | xigoi 4 days ago | parent | next [-] | | Oh my, you sure dodged a bullet. | |
| ▲ | arccy 4 days ago | parent | prev | next [-] | | They probably thought you weren't going to be a good fit for writing idiomatic Go. One of the things many people praise Go for is its standard style across codebases, if you don't like it, you're liable to try and write code that uses different patterns, which is painful for everyone involved. | |
| ▲ | kace91 4 days ago | parent | prev [-] | | Some workplaces explicitly test cultural closeness to their philosophy of work (language, architecture, etc). It’s part trying to keep a common direction and part fear that dislike of their tech risks the hire not staying for long. I don’t agree with this approach, don’t get me wrong, but I’ve seen it done and it might explain your experience. | | |
| ▲ | laserlight 3 days ago | parent [-] | | No need to sugarcoat it. Some places are cults and it's best to avoid them. Good for GP. |
|
|
|
| ▲ | giantg2 3 days ago | parent | prev | next [-] |
| "Concurrency is tricky" This tends to be true for most languages, even the ones with easier concurrency support. Using it correctly is the tricky part. I have no real problem with the portability. The area I see Go shining in is stuff like AWS Lambda where you want fast execution and aren't distributing the code to user systems. |
|
| ▲ | fulafel 2 days ago | parent | prev | next [-] |
| > But I can't help but agree with a lot of points in this article. Go was designed by some old-school folks that maybe stuck a bit too hard to their principles, losing sight of the practical conveniences I think this is a fine "fail-closed" way of language design. For example, Python has gone the other way and language complexity has gotten pretty bad since the small-language days. Trust what you are, don't try to please everyone, lest you become something like C++. Clojure is good in this respect. |
|
| ▲ | guappa 4 days ago | parent | prev | next [-] |
| > The type system is most of the time very convenient In what universe? |
| |
| ▲ | theshrike79 4 days ago | parent [-] | | In mine. It's Just Fine. Is it the best or most robust or can you do fancy shit with it? No But it works well enough to release reliable software along with the massive linter framework that's built on top of Go. | | |
| ▲ | diarrhea 3 days ago | parent [-] | | > massive linter framework I wonder why that ended up being necessary... ;) | | |
| ▲ | theshrike79 2 days ago | parent [-] | | All languages have linters, Go just has a proper ecosystem of them due to the way the language is built. | | |
| ▲ | diarrhea 2 days ago | parent [-] | | $ golangci-lint linters | wc -l
107
That is a long list of linters (only a few enabled by default however). I much prefer less fragmented approaches such as clippy or ruff. It makes for a more coherent experience and surely much higher performance (1x AST parsing instead of dozens of times). |
|
|
|
|
|
| ▲ | tgv 4 days ago | parent | prev | next [-] |
| I find Result[] and Optional[] somewhat overrated, but nil does bother me. However, nil isn't going to go away (what else is going to be the default value for pointers and interfaces, and not break existing code?). I think something like a non-nilable type annotation/declaration would be all Go needs. |
| |
| ▲ | blixt 4 days ago | parent | next [-] | | Yeah maybe they're overrated, but they seem like the agreed-upon set of types to avoid null and to standardize error handling (with some support for nice sugars like Rust's ? operator). I quite often see devs introducing them in other languages like TypeScript, but it just doesn't work as well when it's introduced in userland (usually you just end up with a small island of the codebase following this standard). | | |
| ▲ | tgv 3 days ago | parent [-] | | Typescript has another way of dealing with null/undefined: it's in the type definition, and you can't use a value that's potentially null/undefined. Using Optional<T> in Typescript is, IMO, weird. Typescript also has exceptions... I think they only work if the language is built around it. In Rust, it works, because you just can't deref an Optional type without matching it, and the matching mechanism is much more general than that. But in other languages, it just becomes a wart. As I said, some kind of type annotation would be most go-like, e.g. func f(ptr PtrToData?) int { ... }
You would only be allowed to touch *ptr inside a if ptr != nil { ... }. There's a linter from uber (nilaway) that works like that, except for the type annotation. That proposal would break existing code, so perhaps something an explicit marker for non-nil pointers is needed instead (but that's not very ergonomic, alas). |
| |
| ▲ | bccdee 3 days ago | parent | prev [-] | | Yeah default values are one of Go's original sins, and it's far too late to roll those back. I don't think there are even many benefits—`int i;` is not meaningfully better than `int i = 0;`. If it's struct initialization they were worried about, well, just write a constructor. Go has chosen explicit over implicit everywhere except initialization—the one place where I really needed "explicit." | | |
| ▲ | nothrabannosir 3 days ago | parent [-] | | It makes types very predictable though: a var int is always a valid int no matter what, where or how. How would you design the type system and semantics around initialization and declarations without defaults? Just allow uninitialized values like in C? That’s basically default values with extra steps and bonus security holes. An expansion of the type system to account for PossiblyUndefined<t>? That feels like a significant complication, but maybe someone made it work… | | |
| ▲ | bccdee 2 days ago | parent [-] | | No, I'm saying the language should never permit uninitialized or undefined values in any context. Make `int i;` a compile error and force the user to write `int i = 0;`. Rust does this. |
|
|
|
|
| ▲ | whalesalad 3 days ago | parent | prev | next [-] |
| The remarkable thing to me about Go is that it was created relatively recently, and the collective mindshare of our industry knew better about these sorts of issues. It would be like inventing a modern record player today with fancy new records that can't be damaged and last forever. Great... but why the fuck are we doing that? We should not be writing low level code like this with all of the boilerplate, verbosity, footguns. Build high level languages that perform like low level languages. I shouldn't fault the creators. They did what they did, and that is all and good. I am more shocked by the way it has exploded in adoption. Would love to see a coffeescript for golang. |
| |
|
| ▲ | zozbot234 4 days ago | parent | prev | next [-] |
| Golang is great for problem classes where you really, really can't do away with tracing GC. That's a rare case perhaps, but it exists nonetheless. Most GC languages don't have the kind of high-performance concurrent GC that you get out of the box with Golang, and the minimum RAM requirements are quite low as well. (You can of course provide more RAM to try and increase overall throughput, and you probably should - but you don't have to. That makes it a great fit for running on small cloud VM's, where RAM itself can be at a premium.) |
| |
| ▲ | gf000 4 days ago | parent [-] | | Java's GCs are a generation ahead, though, in both throughput-oriented and latency-sensitive workloads [1]. Though Go's GC did/does get a few improvements and it is much better than it was a few years ago. [1] ZGC has basically decoupled the heap size from the pause time, at that point you get longer pauses from the OS scheduler than from GC. | | |
| ▲ | Capricorn2481 3 days ago | parent [-] | | Do you have a source for this? My understanding is Go's GC is much better optimized for low latency. |
|
|
|
| ▲ | j45 6 hours ago | parent | prev | next [-] |
| I wonder if Go was setup in part to help large, complex or critical codebases on even older old school syntaxes progress relative to where they are. |
|
| ▲ | Mawr 3 days ago | parent | prev | next [-] |
| > I find myself wishing for Optional[T] quite often. Well, so long as you don't care about compatibility with the broad ecosystem, you can write a perfectly fine Optional yourself: type Optional[Value any] struct {
value Value
exists bool
}
// New empty.
func New[Value any]() Optional[Value] {}
// New of value.
func Of[Value any](value Value) Optional[Value] {}
// New of pointer.
func OfPointer[Value any](value *Value) Optional[Value] {}
// Only general way to get the value.
func (o Optional[Value]) Get() (Value, bool) {}
// Get value or panic.
func (o Optional[Value]) MustGet() Value {}
// Get value or default.
func (o Optional[Value]) GetOrElse(defaultValue Value) Value {}
// JSON support.
func (o Optional[Value]) MarshalJSON() ([]byte, error) {}
func (o *Optional[Value]) UnmarshalJSON(data []byte) error {}
// DB support.
func (o *Optional[Value]) Scan(value any) error {}
func (o Optional[Value]) Value() (driver.Value, error) {}
But you probably do care about compatibility with everyone else, so... yeah it really sucks that the Go way of dealing with optionality is slinging pointers around. |
| |
| ▲ | bccdee 3 days ago | parent | next [-] | | You can write `Optional`, sure, but you can't un-write `nil`, which is what I really want. I use `Optional<T>` in Java as much as I can, and it hasn't saved me from NullPointerException. | | |
| ▲ | Mawr 3 days ago | parent [-] | | You're not being very precise about your exact issues. `nil` isn't anywhere as much of an issue in Go as it is in Java because not everything is a reference to an object. A struct cannot be nil, etc. In Java you can literally just `return null` instead of an `Optional<T>`, not so in Go. There aren't many possibilities for nil errors in Go once you eliminate the self-harm of abusing pointers to represent optionality. | | |
| ▲ | bccdee 2 days ago | parent [-] | | Pointers are pretty common in Go though. Not as common as Java, granted—you're not NPEing on an integer—but they still get passed around a decent amount. |
|
| |
| ▲ | kbolino 3 days ago | parent | prev [-] | | There's some other issues, too. For JSON, you can't encode Optional[T] as nothing at all. It has to encode to something, which usually means null. But when you decode, the absence of the field means UnmarshalJSON doesn't get called at all. This typically results in the default value, which of course you would then re-encode as null. So if you round-trip your JSON, you get a materially different output than input (this matters for some other languages/libraries). Maybe the new encoding/json/v2 library fixes this, I haven't looked yet. Also, I would usually want Optional[T]{value:nil,exists:true} to be impossible regardless of T. But Go's type system is too limited to express this restriction, or even to express a way for a function to enforce this restriction, without resorting to reflection, and reflection has a type erasure problem making it hard to get right even then! So you'd have to write a bunch of different constructors: one for all primitive types and strings; one each for pointers, maps, and slices; three for channels (chan T, <-chan T, chan<- T); and finally one for interfaces, which has to use reflection. | | |
| ▲ | Mawr 3 days ago | parent [-] | | For JSON I just marshall to/from: {
"value": "value",
"exists: true,
}
For nil, that's interesting. I've never ran into issues there, so I never considered it. | | |
| ▲ | kbolino 3 days ago | parent [-] | | Ideally, I would want Optional[T] to encode the same as T when a value is present, and to encode in a configurable way when the value is absent. Admittedly, the nothing to null problem exists with *T too, and even with *T and `json:",omitempty"`, you get the opposite problem (null turns to nothing). I didn't think about that at the time, so it's really more of an issue with encoding/json rather than Optional[T] per se. However, you can't implement MarshalJSON and output nothing as far as I know. |
|
|
|
|
| ▲ | jjav 2 days ago | parent | prev | next [-] |
| Go is all right. But in a world where java exists, I still have not seen any reason to consider go. |
|
| ▲ | yubblegum 3 days ago | parent | prev [-] |
| > Concurrency is tricky but You hear that Rob Pike? LOL. All those years he shat on Java, it was so irritating. (Yes schadenfreude /g) |