Up to Main Index Up to Journal for April, 2016 JOURNAL FOR THURSDAY 14TH APRIL, 2016 ______________________________________________________________________________ SUBJECT: Slices, slicing and re-slicing bug DATE: Fri 15 Apr 00:11:50 BST 2016 March was a good month with eight journal entries, a lot of coding and some progress made. It's now April 14th and I haven't made a single entry yet this month. So far this month sucks :( So when I did get a minute or two to myself what was I working on? I have been working on adding player accounts. While doing so I discovered a few resource related issues which I have been dealing with. However testing is slow as it can take half an hour to an hour[1] to get stats back that I can then go through and analyse. I think I've fixed the issues now and will try to get the changes uploaded as soon as I can. I've also found and fixed a bug in the Thing.Remove method. Took a while until I realised what was happening. The reason is obvious when you see it and interesting to fix. The following is a small program to replicate the problem: package main import ( "fmt" ) type units struct { unit []int } func main() { u := &units{[]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}} u.Remove(1,2,3,4,5,6,7,8) fmt.Printf("%v\n", u.Units()) } func (u *units) Remove(e ...int) { for _, e := range e { for j, v := range u.unit { if e == v { u.unit = append(u.unit[:j], u.unit[j+1:]...) break } } } } func (u *units) Units() []int { return u.unit } If we run the above we get the right answer printed: [0 9] Remove has removed the units passed to it (1,2,3,4,5,6,7,8) from the initialised units (0,1,2,3,4,5,6,7,8,9) leaving just 0 and 9. So it works! Yay! So where is the problem? How can this go wrong? What if we wanted to remove all of the elements and changed main to: func main() { u := &units{[]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}} u.Remove(u.Units()...) // <-- Changed this line fmt.Printf("%v\n", u.Units()) } If we re-run the program it now prints: [1 3 5 7] Ya... er... nope, not working. All of the units we specified were not removed. Why and why did we get the sequence 1 3 5 7? The reason for the wrong answer is that the slice of values passed to Remove and the slice of values we are removing from have the same backing array - Units just returns the unit slice. What about the 9? Shouldn't we get the sequence 1, 3, 5, 7, 9? Hrm, no. This is because of the re-slicing and the appending used to perform the actual delete. As more and more values are removed the expression u.unit[j+1...] steps off the end of the slice that has been 'shrinking'. Meanwhile the input slice can see beyond the end of the slice we are removing from as it's view into the backing array isn't changing - values in the backing array are though... that's what I think anyway... Why the odd pattern of values? Removing a value from the u.unit slice also removes it from the passed in slice. When the value is removed all values move back by one slot to 'fill in the gap' but the next range iteration on the input slice moves us on by one slot as well. In effect skipping a value: input parameters: 0 1 2 3 4 5 6 7 8 9 The ^ indicates the current ^ element of the input slice delete 0 from list: 1 2 3 4 5 6 7 8 9 we are working on. ^ next range iteration: 1 2 3 4 5 6 7 8 9 | ^ | skipped! We could fix the problem by modifying the Units method to return a copy of the slice: func (u *units) Units() []int { U := make([]int, len(u.unit), len(u.unit)) copy(U, u.unit) return U } For WolfMUD this wasn't a good option due to performance and additional memory allocations. What else could we do? We could make a copy of the input slice in the Remove method itself: func (u *units) Remove(e ...int) { E := make([]int, len(e), len(e)) copy(E, e) for _, e := range E { for j, v := range u.unit { if e == v { u.unit = append(u.unit[:j], u.unit[j+1:]...) break } } } } Now we only incur the performance hit and allocations when removing values. Can we do better than this? Can we find a solution that avoids the copy but still works if the input is sliced from the value we want to remove? Yes we can :) func (u *units) Remove(e ...int) { for i := len(e) - 1; i >= 0; i-- { for j := len(u.unit) - 1; j >= 0; j-- { if e[i] == u.unit[j] { u.unit = append(u.unit[:j], u.unit[j+1:]...) break } } } } Instead of ranging over the slices in the loops we iterate through the length of the slices - but backwards - and access the slices by index. This means when we remove a value and 'close up the gap' it happens to elements we have already processed and are not interested in any more. Final working code: package main import ( "fmt" ) type units struct { unit []int } func main() { u := &units{[]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}} u.Remove(u.Units()...) fmt.Printf("%v\n", u.Units()) } func (u *units) Remove(e ...int) { for i := len(e) - 1; i >= 0; i-- { for j := len(u.unit) - 1; j >= 0; j-- { if e[i] == u.unit[j] { u.unit = append(u.unit[:j], u.unit[j+1:]...) break } } } } func (u *units) Units() []int { return u.unit } Now it doesn't matter if the passed in values are sliced from the values we are working with or not. Hard to find, hard to explain, simple solution - nice :) -- Diddymus [1] Running the server with 60,000 bots and a few live Telnet sessions. Up to Main Index Up to Journal for April, 2016