6 ms·
Too Much Go Misdirection
- jchw 1y agoThe biggest issue here IMO is the interaction between two things: - "Upcasting" either to a concrete type or to an interface that implements a specific additional function; e.g. in this case Bytes() would probably be useful - Wrapper types, like bufio.Reader, that wrap an underlying type. In isolation, either practice works great and I think they're nice ideas. However, over and over, they're proving to work together poorly. A wrapper type can't easily forward the type it is wrapping for the sake of accessing upcasts, and even if it did, depending on the type of wrapper it might be bad to expose the underlying type, so it has to be done carefully. So instead this winds up needing to be handled basically for each type hierarchy that needs it, leading to awkward constructions like the Unwrap function for error types (which is very effective but weirder than it sounds, especially because there are two Unwraps) and the ResponseController for ResponseWriter wrappers. Seems like the language or standard library needs a way to express this situation so that a wrapper can choose to be opaque or transparent and there can be an idiomatic way of exposing this.
- movpasd 1y agoI'm not sure I fully understand the issue as I don't know Go, but is this something that a language-level delegation feature could help with?
- hello_computer 1y agoIt is the same struggle you can find in any language with private/public props. The stream he wants to read from is actually just a buffer that has been wrapped as a stream, and he’s having a hard time directly accessing the buffer through its wrapper. He could stream it into a new temporary buffer, but he’s trying to avoid that since it’s wasteful. I’ve had the same problem in C++.
- XorNot 1y agoBut the other side of this is that there's a contract violation going on: []byte can be mutated, but io.Reader cannot. When I pass io.Reader I don't expect anything underneath it to be changed except the position. When I pass []byte it might be mutated. So really solving the requires a whole new type - []constbyte or something (in general Go really needs stronger immutability guarantees - I've taken to putting Copy methods on all my struts just so I can get guaranteed independent copies, but I have to do it manually).
- hello_computer 1y agothere is also the specific vs general trade-off: the general (io.Reader) being more flexible, while providing fewer opportunities for optimization. vice versa with the specific—be it []byte, or even []constbyte. i think it is just an inherent struggle with all abstractions.
- jchw 1y ago> But the other side of this is that there's a contract violation going on: []byte can be mutated, but io.Reader cannot. > When I pass io.Reader I don't expect anything underneath it to be changed except the position. When I pass []byte it might be mutated. Where is it written in the contract that the underlying data source of io.Reader is immutable? I do not believe that is true. Even when io.Reader is backed by os.File, the OS may allow the file to be modified while you are reading it, possibly by the same process, using the io.Writer interface. So while it's true that you certainly wouldn't expect an io.Reader itself to be mutable, an object that implements io.Reader certainly can expose interfaces that allow the underlying data source to be mutated. After all, bytes.Buffer is explicitly one such io.Reader; it has a Bytes() method: > "The slice aliases the buffer content at least until the next buffer modification, so immediate changes to the slice will affect the result of future reads." It's documented and totally allowed. Doesn't mean you should do it, but if an io.Reader that's backed by a buffer wants to expose its underlying buffer it isn't an issue of breaking contracts at the very least. Another alternative to exposing an interface that returns []byte is getting clever with interfaces. Go sometimes will try to turn an io.Reader into an io.WriterTo: this can also avoid unnecessary copies. > So really solving the requires a whole new type - []constbyte or something (in general Go really needs stronger immutability guarantees - I've taken to putting Copy methods on all my struts just so I can get guaranteed independent copies, but I have to do it manually). A concept of constness would be nice, but it's trickier than it seems. An immutable type like string is fairly straight-forward. Immutable constant values of primitive types, also straightforward. Of course, Go has both. Constness as in const references to mutable values is weirder, because "const" sounds like it means something is immutable, since it does mean that in many contexts, but actually what it really means is that you can't mutate it, it still might get mutated behind your back, depending on if the actual value is const or not. I think that's at least a little unfortunate. What I think I want is multiple concepts: 1. Variables that can't be re-assigned by default, akin to JavaScript `const` declarations. 2. Immutable const values of composite types, expanding the const primitives Go has today. 3. References that are immutable by default, mirroring Rust. Ideally you could take an immutable reference of constants that are typed, including composite const values, pushing them into rodata like you'd expect. To me this is better than C/C++'s concept of constness because it feels more intuitive when mutability is the exception; Having references marked const explicitly makes it look too similar to defining constants, making it easy to confuse immutability of the values to simply having an immutable reference to a mutable value. Having a `mut` case instead makes it clearer. Can this be done in a theoretical Go-like programming language? Maybe... Will any of it ever be done? Probably not. The most feasible is probably 2., but even that would be pretty awkward due to the lack of immutable references; you'd have to always take a copy to actually use the value.
- msteffen 1y ago> The bytes.Reader should really implement Peek. I’m pretty sure the reason it doesn’t is because this is the only way of creating read only views of slices. And a naughty user could peek at the bytes and then modify them. Sigh. People hate const poisoning, but I hate this more. When I was a Google, a team adjacent to ours was onboarding a new client with performance demands that they could not realistically meet with anything resembling their current hardware footprint. Their service was a stateless Java service, so they elected to rewrite in C++. Now, Java has some overhead because of garbage collection and the JVM, and they hoped that this might move the needle, but what happened was they went from 300qps/core to 1200, with lower tail latency. Literally 3x improvement. Why? Probably a lot of reasons, but the general consensus was: Java has no const, so many of Google’s internal libraries make defensive copies in many places, to guarantee immutability (which is valuable in a highly concurrent service, which everything there is). This generates a huge amount of garbage that, in theory, is short-lived, rarely escapes its GC generation, and can all be cleaned up after the request is finished. But their experience was that it’s just much faster to not copy and delete things all over the place. Which you can often avoid by using const effectively. I came to believe that this was Java’s biggest performance bottleneck, and when I saw that Go had GC with no const, I figured it would have the exact same problem
- hinkley 1y agoJava has a little const. Strings are immutable. You can make objects with no mutations, so you can make read only collections fairly easily which is usually where const becomes a problem. But then you have for instance Elixir, where all functions are pure, so mutating inputs to outputs takes a ton of copying, and any data structure that is not a DAG is a gigantic pain in the ass to modify. I lost count of how many tries it took me to implement Norvig’s sudoku solver. I kept having to go back and redesign my data structures every time I added more of the logic. [edit to add]: DTOs exist in Java because some jackass used the term “Value Object” to include mutation despite large swaths of the CS world considering VOs to be intrinsically const. So then they had to make up a new term that meant Value Object without using the concept they already broke.
- throwawaymaths 1y ago
- 90s_dev 1y ago> Now, why doesn’t bytes.Reader implement Peek? It’s just a byte slice, it’s definitely possible to peek ahead without altering stream state. But it was overlooked, and instead this workaround is applied. When I first looked at Go, it seemed to have far too many layers of abstraction on top of one another. Which is so ironic, considering that's one of the main things it was trying to fix about Java. It ended up becoming the thing it fought against.
- treyd 1y agoI would agree with you but not so much here specifically. It's much more true with how goroutines and channels work, in that they're too unstructured and don't compose well, which necessitates needing to make ad-hoc abstractions around them.
- rester324 1y agoYeah, they copied that too and the smugness of the early java community. One example below, although I frequently bump into conversations in the golang community in the same vein just don't bother to save them: https://github.com/golang/go/issues/29461 https://github.com/golang/go/issues/29461
- swisniewski 1y agoThere's a much simpler way to do this: If you want your library to operate on bytes, then rather than taking in an io.Reader and trying to figure out how to get bytes out of it the most efficient way, why not just have the library taken in []byte rather than io.Reader? If someone has a complex reader and needs to extract to a temporary buffer, they can do that. But if like in the author's case you already have []byte, then just pass that it rather than trying to wrap it. I think the issue here is that the author is adding more complexity to the interface than needed. If you need a []byte, take in a []byte. Your callers should be able to figure out how to get you that when they need to. With go, the answer is usually "just do the simple thing and you will have a good time".
- deleted 1y ago[deleted]
- TheDong 1y agoThe author is trying to integrate with the Go stdlib, which requires you produce images from an 'io.Reader". See https://pkg.go.dev/image#RegisterFormat https://pkg.go.dev/image#RegisterFormat Isn't using the stdlib simpler than not for your callers? I also often hear gophers say to take inspiration from the go stdlib. The 'net/http' package's 'http.Request.Body' also has this same UX. Should there be `Body` and `BodyBytes` for the case when your http request wants to refer to a reader, vs wants to refer to bytes you already have?
- jchw 1y agoThe BodyBytes hypothetical isn't particularly convincing because you usually don't actually have the bytes before reading them, they're queued up on a socket. In most cases I'd argue it really is idiomatic Go to offer a []byte API if that can be done more efficiently. The Go stdlib does sometimes offer both a []byte and Reader API for input to encoding/json, for example. Internally, I don't think it actually streams incrementally. That said I do see why this doesn't actually apply here. IMO the big problem here is that you can't just rip out the Bytes() method with an upcast and use that due to the wrapper in the way. If Go had a way to do somehow transparent wrapper types this would possilby not be an issue. Maybe it should have some way to do that.
- liampulles 1y agoThe reason interface smuggling exists as a pattern in the Go standard library and others is because the Go team (and those who agree with its philosophy) take breaking API changes really seriously. It is no small feat that Go is still on major version 1.
- treyd 1y agoWouldn't you say that it's a design oversight that the interface system leads to tight constraints on what you can do without breaking APIs?
- liampulles 1y agoI can't call it a design oversight no, because I'm not sure what reasonable alternatives were considered before Go v1 was released. I also don't have context of all the decision factors that went into Go's spec. To be honest, I'm not anywhere near an expert on programming language design - I'm probably the wrong person to ask. I am thankful that they haven't broken the spec to change that design, but maybe others don't care about that as much as I do.
- hkpack 1y agoIt seems that go library is ok with you paying the performance price when using io.Reader/io.Writer on memory structures. You can write clean idiomatic code, but it won’t be the fastest. So for maximum results you should always do everything manually for your use case: i.e. don’t use additional readers/writers and operate on []byte directly if that is what you are working with. I think it is mostly a good thing - you can quickly write simple but slower code and refactor everything later when needed.
- millipede 1y agoType inspection is the flaw of Go's interface system. Try to make a type that delegates to another object, and the type inspection breaks. It's especially noticeable with the net/http types, which would be great to intercept, but then breaks things like Flusher or Hijacker.
- 38 1y ago> What I would like is for my image decoding function to notice that the io.Reader it has been given is in fact a bytes.Reader so we can skip the copy. What a terrible idea. If you want bytes.reader, then use that in the function signature, or better yet just a byte slice. It should have been a red flag when your solution involves the unsafe package
- Groxx 1y agoI think you kinda missed the point. The point is that trying to make a user-friendly API with the familiar, highly composable, and extremely common io.Reader that everything (including Go's stdlib) encourages you to use ends up putting you in this unfortunate design corner if you also care about performance. It's frustration about getting close to a good API, but not having any reasonable way to close the final gap, forcing you do you to go stuff like you mentioned: have multiple near-identical APIs for performance, and needing your users to understand and use them correctly to get a good result.
- XorNot 1y agoThe benefit seems fictional though. When is the user going to have all the bytes in memory but only have an io.Reader? Probably never. If I have a reader it's because the bytes are coming from something which itself does not make that promise. Like the most common application would be an os.File. If I do have all the bytes in memory, then I have a []byte array, know I have it, and can use the []byte interface you must've implemented internally to use this speed up.
- Groxx 1y agoI think you kinda missed the point. The point is that trying to make a user-friendly API with the familiar, highly composable, and extremely common io.Reader that everything (including Go's stdlib) encourages you to use ends up putting you in this unfortunate design corner if you also care about performance. It's frustration about getting close to a good API, but not having any reasonable way to close the final gap, forcing you do you to [do] stuff like you mentioned: have multiple near-identical APIs for performance, and needing your users to understand and use them correctly to get a good result.
- bobbylarrybobby 1y agoI am not a gopher, so this may be a dumb question: when an io.Reader produces a buffer of its contents, does it not have the option of just returning the buffer it wraps if it does in fact wrap a buffer? Something like (pseudocode) `if self isa BufferedReader { self.takeBuffer() } else { let buffer = newBuffer(); self.fill(buffer); buffer }`.
- masklinn 1y ago1. Like read(2), io.Reader.Read takes a slice parameter to which it writes, it doesn't return one. This is better for a "legitimate" read stream as the caller can amortise allocations. 2. And Read takes the slice by value, so the length, capacity, and buffer pointer are copied into the callee. This gives no way of "swapping buffers", even ignoring that the caller may have issues getting back a slice with a completely different size and capacity than they sent / expected.
- nulld3v 1y agoI am once again begging for this to be implemented: https://github.com/golang/go/issues/4146 https://github.com/golang/go/issues/4146
- draw_down 1y ago[dead]
- nottorp 1y agoInteresting, I don't know Go (yet) but I was messing with it the other weekend. Can someone please summarize this []byte vs somethingReader thing for me? Assume I can program, just not familiar with Go. I was reading off sockets and it looked to me that the example code (i randomly ran into) had too many Reader something or other. Edit: Ok, I know what a streaming class does, they're available in many frameworks. I'm more interested in why you'd get forced to use them in the context of the Go standard library. Are they mandatory for sockets? Or for interacting with other common functions that I'd use to process the data out of my sockets? I just wanted to read up to a new line or a hard coded size limit from a socket... ;) Without getting accidentally quadratic in either cpu use or memory use...
- mholt 1y ago[]byte is a buffer. (io.)Reader is any type that can read bytes, like as part of a stream. There are `bytes.Reader` and `bufio.Reader` and `strings.Reader`, and many others, depending on what you're reading. These three types allow you to read []byte, any buffered input, and string, respectively, as streams. Streaming is more efficient for large pieces of data (files, etc; whatever you have), but the buffer is usually easier to work with and grants more flexibility.
- ashishb 1y agoImagine you are reading files which can be of varying size (KB to GB). A byte array will do a contiguous allocation of the file size. A Reader can be much more thoughtful. And I say "can be" because someone can make Reader as inefficient as a byte array. Or they can read in chunks. For example, if you are trying you read exif data or reading up to first N bytes, Reader is a superior approach.
- masklinn 1y agobyte[] is a bytes buffer, a sequence of bytes. io.Reader is an abstraction around a read stream. You can adapt a byte[] to a Reader by wrapping it in a bytes.Reader, that way if a function needs a reader and you have bytes, you can give them your bytes. The problem TFA has, is that bytes.Reader implies a copy: it's going to read data into a second internal slice. So when all their library needs is the bytes, they could use the bytes themselves, and avoid a potentially expensive copy. Obviously you could just have a second entry point which takes straight byte[] instead of a reader, but in this case they're trying to conform to the standard library's image module[1] which does not expose a bytes interface and conditionally adds further indirection layers. [1]: https://pkg.go.dev/image https://pkg.go.dev/image
- binary132 1y agoBreaking virtual types is bad.
- eximius 1y agoThis really feels like trying to use Go for a purpose that it is inherently not designed for: absolute performance. Go is a fantastic case study in "good enough" practical engineering. And sometimes that means you can't wring out the absolute max performance and that's okay. It's frustrating, but it is fulfilling it's goals. It's goals just aren't yours.
- callc 1y agoAfter using Go for large and medium projects, with hundreds and < 10 people respectively, I’m left with a desire for a better language. I enjoy the simplicity of Go, but the tradeoff of Go’s imposed limitations (concurrency model, no subtype polymorphism in place of parametric polymorphism, no methods on external types, etc) feel like strict limitations without much benefit. (Minus labels and gotos, that’s great design) The speed I don’t care about The model of loose global functions for fundamental operations on built in data types is poor design. append(), delete(), close(), etc. How about make these methods on their respective types? Overall coding in Go feels like coding in a pre-alpha language. Syntax-wise, not the top notch tooling and packaging.
- ncruces 1y agoInstead of focusing on bytes.Reader, the author could maybe propose that bytes.Buffer (which already has a Bytes method) implements Peek as well. bytes.Buffer already has many methods exposing the internal buffer, so one more is not insurmountable.
- hinkley 1y agoThe title is a bit tortured. Too much indirection I would agree is misdirection. “Too much misdirection” sounds like you’ve gone past bad design into clown car.