Slices, backing array, goroutines, perhaps Considerations for Go2

252 views
Skip to first unread message

atd...@gmail.com

unread,
Oct 26, 2019, 7:40:23 AM10/26/19
to golang-nuts
Hello,

I've just realized that slices were much more delicate to use than initially thought. The main issue is that when slices share the same backing array, it is too easy to inadvertently have an unprotected concurrent Read/Write  if one is not careful.
That can easily happen for a struct that has a field of type slice for instance. The object might be safe to copy and pass around to multiple goroutine but reading a value from the slice field can still be problematic if there is a write at the same time on one of the object's slice field.

This is not a proposal but for later, would it make sense to have slices become real snapshots (immutable) or copy/allocate on mutation?
It will probably have perf repercussions.

Or alternatively a tool that alerts of cases where there are such case of unprotected concurrent use (perhaps the race detector already does this)

Even better is if that's a non argument because the cases where it's happen in real world code is sparse but I must admit that it took me a long time to think about this and now, I'm worried that it's a mistake too easily made.

Also, I do not remember the specifics of the map code, but what happens for a map of slices? Do the slice elements share the backing array? 



Gert

unread,
Oct 26, 2019, 8:27:06 AM10/26/19
to golang-nuts
Sorry, the performance cost is to high. But you can practice a few programming habits that can prevent this, like use x = append(x, y) and never x = append(y, x) Basically we only need a Go proverb bible mentioned in one of Rob Pike's talks and we will be fine :)

atd...@gmail.com

unread,
Oct 26, 2019, 9:02:37 AM10/26/19
to golang-nuts
Hi,

Thank you for the response.

My issue is not really about append but can be illustrated by this :

Gert

unread,
Oct 26, 2019, 1:20:31 PM10/26/19
to golang-nuts
Correct, so basically avoid x := y[i:j] if you actually meant copy

https://golang.org/ref/spec#Appending_and_copying_slices

var a = [...]int{0, 1, 2, 3, 4, 5, 6, 7}
var s = make([]int, 6)
var b = make([]byte, 5)
n1 := copy(s, a[0:])            // n1 == 6, s == []int{0, 1, 2, 3, 4, 5}
n2 := copy(s, s[2:])            // n2 == 4, s == []int{2, 3, 4, 5, 4, 5}
n3 := copy(b, "Hello, World!")  // n3 == 5, b == []byte("Hello")

atd...@gmail.com

unread,
Oct 26, 2019, 1:49:50 PM10/26/19
to golang-nuts
I get that we can do that but it's not strictly about slices operations such as appending or copying.
Of course, we could copy slices all the time but then there is no benefit in having them and it is not ergonomic as your example shows, nor does it fit your performance cost requirements.

If you look at the code snippet I posted, I am merely assigning a value of type slice to a struct field. Now, that struct can be copied a number of times without its actual value changing. It looks immutable in some sense.  BUT, it's actually implicitly tied to an object that is mutable (backing array) and nowhere have we made sure that the mutation of this backing array is safe.

atd...@gmail.com

unread,
Oct 26, 2019, 1:57:19 PM10/26/19
to golang-nuts
To add to this issue, that's a problem that is also related to how type equality is treated in the spec and deep equality. If we have some kind of "snapshot" slices for instance, an equality can be determined for these (with some other things to consider) and for structs with fields that are "snapshot" slices.

Ian Lance Taylor

unread,
Oct 26, 2019, 3:47:39 PM10/26/19
to atd...@gmail.com, golang-nuts
On Sat, Oct 26, 2019 at 10:50 AM atd...@gmail.com <atd...@gmail.com> wrote:
>
> I get that we can do that but it's not strictly about slices operations such as appending or copying.
> Of course, we could copy slices all the time but then there is no benefit in having them and it is not ergonomic as your example shows, nor does it fit your performance cost requirements.
>
> If you look at the code snippet I posted, I am merely assigning a value of type slice to a struct field. Now, that struct can be copied a number of times without its actual value changing. It looks immutable in some sense. BUT, it's actually implicitly tied to an object that is mutable (backing array) and nowhere have we made sure that the mutation of this backing array is safe.

For better or for worse, the Go language has shared state. This is
most obvious when using pointers: if you copy a pointer, you do not
copy the memory to which that pointer points. If that is clear, then
the thing to remember about slices is that they are pointers.
Pointers in Go do not permit arithmetic. Slices in Go are pointers
that permit bounded arithmetic.

Ian

atd...@gmail.com

unread,
Oct 26, 2019, 4:22:42 PM10/26/19
to golang-nuts
Oh, I hadn't thought about it that way! Interesting.
I still feel like there should be a way to reign in the amount of shared state in the cases of these "fat" pointers a little bit. Or at least, inspect it, warn about it more bceause most people seem to think that slices are just infinite arrays (hence the need for the extra-level of indirection/pointer). 
But perhaps, the coding patterns/idioms make it a non issue.  For future considerations I guess.
Ian

Gert

unread,
Oct 26, 2019, 7:04:49 PM10/26/19
to golang-nuts
On Saturday, October 26, 2019 at 10:22:42 PM UTC+2, atd...@gmail.com wrote:

On Saturday, October 26, 2019 at 9:47:39 PM UTC+2, Ian Lance Taylor wrote:
On Sat, Oct 26, 2019 at 10:50 AM atd...@gmail.com <atd...@gmail.com> wrote:
>
> I get that we can do that but it's not strictly about slices operations such as appending or copying.
> Of course, we could copy slices all the time but then there is no benefit in having them and it is not ergonomic as your example shows, nor does it fit your performance cost requirements.
>
> If you look at the code snippet I posted, I am merely assigning a value of type slice to a struct field. Now, that struct can be copied a number of times without its actual value changing. It looks immutable in some sense.  BUT, it's actually implicitly tied to an object that is mutable (backing array) and nowhere have we made sure that the mutation of this backing array is safe.

For better or for worse, the Go language has shared state.  This is
most obvious when using pointers: if you copy a pointer, you do not
copy the memory to which that pointer points.  If that is clear, then
the thing to remember about slices is that they are pointers.
Pointers in Go do not permit arithmetic.  
 
Slices in Go are pointers
that permit bounded arithmetic.

Oh, I hadn't thought about it that way! Interesting.
I still feel like there should be a way to reign in the amount of shared state in the cases of these "fat" pointers a little bit. Or at least, inspect it, warn about it more because most people seem to think that slices are just infinite arrays (hence the need for the extra-level of indirection/pointer). 
But perhaps, the coding patterns/idioms make it a non issue.  For future considerations I guess.

I believe it's going to be to hard to write a go vet analyser that could warn to detect unintended state change on a array from two different pointers. Go made it this far without many programmers throwing monitors out of the window so I think will be fine by making sure we document slices pointers and interfaces in detail. Yes does 3 pages going to be heavy to understand but if you just remember when you encounter weird stuff to just look again at does 3 pages.

roger peppe

unread,
Oct 27, 2019, 4:54:29 AM10/27/19
to Gert, golang-nuts


On Sun, 27 Oct 2019, 00:04 Gert, <gert.c...@gmail.com> wrote:
I believe it's going to be to hard to write a go vet analyser that could warn to detect unintended state change on a array from two different pointers.

Isn't that why we have the race detector?

--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/30ac16bc-bc70-40a6-bc79-f41e3536d144%40googlegroups.com.

Gert

unread,
Oct 27, 2019, 8:33:36 AM10/27/19
to golang-nuts

On Sunday, October 27, 2019 at 9:54:29 AM UTC+1, rog wrote:

On Sun, 27 Oct 2019, 00:04 Gert, <gert....@gmail.com> wrote:
I believe it's going to be to hard to write a go vet analyser that could warn to detect unintended state change on a array from two different pointers.

Isn't that why we have the race detector?

I think it only catches concurrent access to the same address, so not sure if it catches everything.

roger peppe

unread,
Oct 27, 2019, 4:20:44 PM10/27/19
to Gert, golang-nuts
If two things aren't writing concurrently to the same address, what's the problem?

--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.

andrey mirtchovski

unread,
Oct 27, 2019, 4:27:42 PM10/27/19
to roger peppe, Gert, golang-nuts
>> I think it only catches concurrent access to the same address, so not sure if it catches everything.
>
>
> If two things aren't writing concurrently to the same address, what's the problem?

I took that to mean 'things may be updating concurrently at different
addresses inside an array backing the same slice'. But the race
detector will flag as soon as a read is attempted by another goroutine
on any of those.

Dan Kortschak

unread,
Oct 27, 2019, 5:32:11 PM10/27/19
to roger peppe, Gert, golang-nuts
This is not necessarily due to races. You can have this exact situation
occurring in single threaded code. I don't think any mention of race
issues was made here.
> > https://groups.google.com/d/msgid/golang-nuts/30ac16bc-bc70-40a6-bc79-f41e3536d144%40googlegroups.com?utm_medium=email&utm_source=footer
> > >
> > .
> >
>
>

roger peppe

unread,
Oct 28, 2019, 4:44:24 AM10/28/19
to Dan Kortschak, Gert, golang-nuts


On Sun, 27 Oct 2019, 21:31 Dan Kortschak, <d...@kortschak.io> wrote:
This is not necessarily due to races. You can have this exact situation
occurring in single threaded code. I don't think any mention of race
issues was made here.

The thread subject mentions goroutines and the original message talks specifically about concurrent access, which sounds very much like it's race-related to me.

atd...@gmail.com

unread,
Oct 28, 2019, 5:17:10 AM10/28/19
to golang-nuts
Yes, the issue is twofold.

Because everything is passed by "copy", it might feel safe to pass around structs with slice fields, especially and perhaps unknowingly as "interior fat pointers" in structs. 
But, one should remember that these objects are always shallow copies in the sense that map/slices/functions will refer to the same underlying elements and not be dissociated. 
Thus it could be nice to have a way to enforce deeper copies at the language level as well.

The second issue that is related to the first one is that in the case of mutliple access by different goroutines, 
this problem can be compounded since the objects can seem to be naturally thread safe but the slice operations for instance are not. 
Perhaps is it even the same issue if we send slices through channels if for some reason, the backing arrays are shared, I don't know. 

Main thing is that I don't trust the fact that many beginners will have thought about this (or perhaps it is just me who didn't). 
Especially when passing objects to different functions that may mutate an object internals thinking it is just a copy.

On Monday, October 28, 2019 at 9:44:24 AM UTC+1, rog wrote:


On Sun, 27 Oct 2019, 21:31 Dan Kortschak, <d...@kortschak.io> wrote:
This is not necessarily due to races. You can have this exact situation
occurring in single threaded code. I don't think any mention of race
issues was made here.

The thread subject mentions goroutines and the original message talks specifically about concurrent access, which sounds very much like it's race-related to me.

On Sun, 2019-10-27 at 08:53 +0000, roger peppe wrote:
> On Sun, 27 Oct 2019, 00:04 Gert, <gert....@gmail.com> wrote:
>
> > I believe it's going to be to hard to write a go vet analyser that
> > could
> > warn to detect unintended state change on a array from two
> > different
> > pointers.
> >
>
> Isn't that why we have the race detector?
>
> --
> > You received this message because you are subscribed to the Google
> > Groups
> > "golang-nuts" group.
> > To unsubscribe from this group and stop receiving emails from it,
> > send an

Dan Kortschak

unread,
Oct 28, 2019, 5:22:48 AM10/28/19
to roger peppe, Gert, golang-nuts
You are absolutely right. Apologies.

Wojciech S. Czarnecki

unread,
Oct 28, 2019, 6:25:48 AM10/28/19
to golan...@googlegroups.com
On Sun, 27 Oct 2019 20:20:07 +0000
roger peppe <rogp...@gmail.com> wrote:

> If two things aren't writing concurrently to the same address, what's the
> problem?

I've got an impression that OP is concerned about arriving at
inconsistent/"impossible" shared state (due to poorly thought architecture).

30-20yrs ago this kind of bug have had haunted codebases still using global
state, where procedure X set some state in S and precomputed S' that was then
being used by procedure Z; but in meantime the S, and only S, had got modified
(by some procedure Y in the middle). So, from the pov of Z (that assumed
S' ← S) we have had got to an "impossible state".
In event-loop driven programs often this happened in seemingly random ways.

@Gert, <gert.c...@gmail.com>
This indeed can easily can be "achieved" in a single threading Go code that
uses slices (pointers) to the shared state. It was, it is, and it always will
be a programmer's error. More, no known to me code analysis tool could
comprehend changes to your shared state in full and then flag only the wrongs.
An AI capable of that would also be capable of replacing us at our desks ;)


Hope this helps,

--
Wojciech S. Czarnecki
<< ^oo^ >> OHIR-RIPE
Reply all
Reply to author
Forward
0 new messages