5 ms·
Hmm, the first few WATs are pretty bad examples. Any developer, gopher or not, should know that, when you reassign the value of a function parameter, the variab
by sacado2 8y ago
Hmm, the first few WATs are pretty bad examples. Any developer, gopher or not, should know that, when you reassign the value of a function parameter, the variable when the function returns still has its original value. It's pass-by-reference 101. Now, sure, slices and the way append work are a bit complicated to grasp at first, especially if you never used C, but this is not a WAT.
A lot of WATs are more bad practice than problems in the language, IMO (like WAT 8, modifying the return value twice in deferred functions, with the order being significant: who the hell writes such a code?)
A few ones, though, really are problematic. For instance I've been bitten more than once by variable shadowing, especially with error values. IMO, this is the weakest point of Go.
- masklinn 8y ago> Now, sure, slices and the way append work are a bit complicated to grasp at first slices are not complicated, the issue is append, mutable slices and backing arrays being shared, that's what's really fucky about it. And that interaction is absolutely a WAT. Especially the ability to separately append to two slices with the same backing array. > A lot of WATs are more bad practice than problems in the language, IMO Much the same could be said about the original presentation. There absolutely are non-WAT sections in that article though. (4) is one.
- kodablah 8y ago> And that interaction is absolutely a WAT I'm not sure views into a fixed-sized array is really confusing/strange at all. Whether you create multiple views in the array, mutate the views, etc. All interactions that occur on slices and arrays make plenty of sense. How you'd write something like append yourself if "Slice" was your type w/ a backing array, a start index, and a length is quite straightforward.
- TheDong 8y ago> Whether you create multiple views in the array, mutate the views Whether there are multiple views or not is actually an implementation detail, and whether "append" chooses to allocate memory or not will change whether there are multiple copies or multiple views of the array. All of that behavior is something you absolutely cannot rely on.
- kodablah 8y agoCan I not rely on append only allocating when the values to append won't fit in the remaining capacity of the underlying array starting from the end of the slice? Some articles indicate this and definitely don't seem surprising [0]. Granted I don't know the implementation details, but how do copies of the array or numbers of slices of the array affect that? Regardless, with Go as opposed to lower level languages, building reliance on whether functions do memory allocation (even built in ones) is folly. 0 - https://blog.golang.org/go-slices-usage-and-internals https://blog.golang.org/go-slices-usage-and-internals
- slrz 8y agoThe behaviour of append and the conditions where it will allocate a new backing array are specified and part of the language. Implementation details don't even enter the picture here. You can absolutely rely on that not changing.
- masklinn 8y ago> I'm not sure views into a fixed-sized array is really confusing/strange at all. Again, as I already stated in the previous comment the issue is not the slices themselves. It's that they're doubling up as vectors and they're shitty at it: you can share a backing buffer between mutable slices and append to both and then all hell breaks loose. That is the issue. And that is why Rust doesn't have that issue despite having the same concept of slices: you can't grow a slice, and you can't have multiple mutable slices to the same backing buffer.
- kodablah 8y agoI mean, by that measure C/C++ and many others have the same problems. Your issue about lack of compiler-enforced reuse checks after invalidation is more with the language than this feature. While I agree generally, I don't see how slices are any more of a problem or surprising here than any other mutable reference that may or may not be mutated conditionally by a function.
- masklinn 8y ago> I mean, by that measure C/C++ and many others have the same problems. No. C only has raw pointers so it's not trying to pretend slices are vectors, and C++ has actual functioning vectors with "slicing" being either a copy of the subvector or an iterator. Either way there is no confusion as to the capabilities of what you're given and its relation to the original data.
- millstone 8y agoThis is absolutely one of the worst aspects of Go. When you call append() on a slice, and then modify the contents of the result, you may or may not modify the original slice. In a language that encourages concurrency, this indeterminism should be jaw-dropping. How many Go programs are safe only accidentally via an array's allocation policy (especially given how much code it requires to duplicate a slice)? Can you think of any other language that has such a design?
- kodablah 8y agoIt's not indeterminate, just conditional at runtime, no different than any other may-mutate-parameter-reference function. It fits Go's philosophy of foot guns for the sake of performance/simplicity (be it compiler or runtime performance/simplicity). Use of append by devs should invalidate any future uses of the parameter, but this is better than forcing a copy or abstracting implementation details of linked arrays. > Can you think of any other language that has such a design? Sure, happens all the time because it fits the machine naturally though sometimes it may be hidden with unnecessary runtime costs by many languages that do force copies or reallocations due to immutability. C++ is a language that doesn't hide it. Your argument is like saying you're surprised that std::vector::emplace_back may or may not alter the array I had kept from std::vector::data previously.
- deleted 8y ago[deleted]
- coldtea 8y ago>A lot of WATs are more bad practice than problems in the language, IMO (like WAT 8, modifying the return value twice in deferred functions, with the order being significant: who the hell writes such a code?) Isn't that the tired old argument from the C camp? "If you are careful you can write perfectly fine C without bugs". DUH! The whole idea is for the compiler and types to catch as much of this crap as possible, not to add accidental mental burden to the programmer.
- SOLAR_FIELDS 8y agoWhile I agree that compiler should eventually be updated to prevent this sort of behavior or something similar, I agree with the grandparent here. I had same sentiment as the grandparent, in that for several of these errors, if you end up causing them it just belies a lack of understanding of software development in general. It’s great if we can improve compilers to teach people about these sort of behaviors, but they don’t really fall under the definition of WAT that I think is being referenced in the article. That is, a WAT behavior like this is something you would normally expect to behave in a certain way, but because of some language idiosyncrasy or design decision in the language it actually does something different.
- stcredzero 8y agoMy first thought was that a lot of these WAT should elicit an eye roll from anyone who took Comp Sci. My eventual conclusion was that this was just a form of stealth basic Comp Sci education for language hipsters.
- jjnoakes 8y ago> It's pass-by-reference 101. Is it though? Slices seem to be passed by value, where the value is a pointer and some bookkeeping information (like a C struct of (data, len, capacity)). Some operations manipulate just data (which is visible in the caller, since that 1/3 of the "struct" was a pointer passed by value) and some operations manipulate len and/or capacity (which is not visible in the caller, since that 2/3 of the "struct" was also passed by value). Seems more complex than just hand-waving the details away with "pass-by-reference 101".
- lann 8y agoAn illustation of append being confusing: https://play.golang.org/p/WGwp2cM6E1B https://play.golang.org/p/WGwp2cM6E1B
- deleted 8y ago[deleted]
- scarejunba 8y agoThis would have been a way better WAT.
- adwhit 8y agoIndeed, less 'confusing' and more 'broken'.
- kbenson 8y agoIt's pass by reference 101 where by reference is actually passing a struct by value, and the struct has a reference to more data, so sometimes the backing data is updated and the struct isn't... So, probably more like pass-by-reference-and-by-value 201.
- andrewstuart2 8y agoGo is only pass by value. Much in the same way JavaScript is (the best description for JS, IMO, is reference by value). Even if you pass a pointer, the pointer is explicitly copied to a new location in memory, which is why assignment to the pointer can overwrite the pointer, but not affect the original pointer (the memory location value inside the pointer-typed variable) outside the function scope. Pass by reference is when assignment to a parameter name is transitive to the caller's scope, and can be seen in C#. [1] [1] https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/ref https://docs.microsoft.com/en-us/dotnet/csharp/language-refe...
- jrockway 8y agoThe slices are actually confusing and I've written quite a few test programs to teach myself the WATs because the behavior is not obvious from the syntax. Remember, slices are very mutable, regardless of the lack of a * in the function parameter. This works as you'd expect: type thing struct { foo int } func f(x thing) { x.foo = 42 } func main() { var x thing x.foo = 1234 fmt.Printf("x before: %v\n", x) f(x) fmt.Printf("x after: %v\n", x) } This prints 1234 and 1234 as you'd expect. That's pass by value in action. But what if we mutate a slice? func f(x []int) { x[1] = 42 } func main() { x := []int{1, 2, 3} fmt.Printf("x before: %v\n", x) f(x) fmt.Printf("x after: %v\n", x) } Of course, this prints [1 2 3] and [1 42 3]. While you're passing the slice by value, the backing array is not copied. As you write more go, you will begin to fully see the existence of a backing array. Try this program: func main() { x := []int{1,2,3,4} y := x[1:2] y[0] = 42 fmt.Printf("x: %v, y: %v\n", x, y) } You know that [:] does no copying (there is a copy() function that copies stuff) and just shares the same backing array between each slice, so you aren't surprised when this prints "x: [1 42 3 4], y: [42]". Now, add some knowledge from the documentation about how append() works. You use the idiom slice = append(slice, element) because the documentation says, sometimes the slice is updated in place when there is enough capacity for that to occur, and sometimes the slice is copied to a new slice and returned. This then leads to your "WAT" in WAT 2. Because you _know_ that the data you appended is actually in the backing array, but for some reason Go isn't showing it to you. You can see this in action with a very poor example I just wrote: func main() { x := []int{1, 2, 3, 4, 5, 6} y := x[0:0] fmt.Printf("x: %v, y: %v\n", x, y) fmt.Printf("intermediate result that's not saved: %v\n", append(y, 9, 8, 7, 6, 5)) fmt.Printf("x: %v, y: %v\n", x, y) } After all of this, x is [9 8 7 6 5 6]. So you _know_ that append is more than happy to mess with your backing array. You just need a slice with the right length to be able to see all the data in there. For that reason, I think WAT 2 is worthwhile. Most people know _just enough_ about slices to be dangerous, and so are surprised when there is an additional complication that they haven't thought about.
- randomdata 8y ago> This prints 1234 and 1234 as you'd expect. Until it doesn't. type thing struct { foo *int } func f(x thing) { *x.foo = 42 } func main() { v := 1234 var x thing x.foo = &v fmt.Printf("x.foo before: %v\n", *x.foo) f(x) fmt.Printf("x.foo after: %v\n", *x.foo) } Or maybe that is what you'd expect, but then slices shouldn't seem so mysterious.
- deleted 8y ago[deleted]