It is perhaps easiest if I go through the conditions which must be met for data to be reclaimed by the Operating System:
First, the data must be unreachable by goroutines through a path of pointers. When you nil the object, you break such a path, but you must make sure there are no other such paths to the object. Once there is no reachability of the object, it is eligible for collection.
Second, garbage collection must run. It runs periodically, but it can take some time before it does so, and if you check memory usage right after you've freed up data, it might not have run yet. You can run Go with GC debug output (env GODEBUG=gctrace=1 ...) to see when it actually runs. This is a property of using a mark'n'sweep garbage collection strategy as the memory is scavenged for unreachable data immediately. It is somewhat in contrast to reference counting methods which will usually free up the memory quicker.
Third, the GC must have some excess data it wants to give back to the OS. Usually it likes to reuse a memory space if it is needed shortly and will only give memory back eventually if the program consistently runs in less memory.
There are some subtle things you might want to look out for however. When you unmarshal from the string into the concrete report type, you should probably avoid keeping a slice to the underlying source string. Otherwise, it will keep said string alive for the duration of the reports existence. Explicitly copying data and making sure you have no reference to the source string is one thing that is important to get right, especially if the source string is much larger than the report in memory size. Another thing is that I would just do `delete(reports, timestamp)` since it is a no-op if timestamp doesn't exist, and deletion effectively breaks the pointer anyway.
I hope this helps somewhat in narrowing down what could be wrong.
J.