Comment by jjcm

Comment by jjcm 9 days ago

60 replies

Great tips in here - I was not aware of `go vet` nor `go test -race`.

FWIW, while go is not memory safe, I do find that it's much easier to be safe in go than it is in other languages. Its verboseness lends to a very clear understanding of what's happening in any given function. I absolutely hated this at the start, but now ~3 years into maintaining a go codebase, I find it quite nice both for debugging as well as editing old code. I know exactly what each function does, and what the structure of data is in any given context.

Another interesting side effect is that AI tools seem to work amazingly well with golang, given how context is often local to the function.

uluyol 9 days ago

Go very much is memory safe in the absence of data races.

Data races cause issues in all languages, though it's fair to say that Go is affected slightly more than languages like Java. Rust is a bit special by making data races hard to trigger (impossible in safe code IIUC), but this is not typical.

  • pjmlp 9 days ago

    Kind of, regarding Rust.

    It is impossible in the context of having all threads accessing in-process memory.

    If the data can be accessed externally, regardless of the guarantees being uphold on the Rust side, there are no guarantees from third parties accessing the same data.

    It also doesn't prevent other race issues with external data.

    • galangalalgol 9 days ago

      Memory like that needs to be wrapped with unsafe for access, there is the volotile crate to mark stuff like that so the compuler won't optimize it away.

      Other than rust haskell seems like the other primary candidate for memory safety even across threads.

      • pjmlp 9 days ago

        Yes, but it doesn't guarantee changes occurring from third parties, even if everything is done correctly on Rust side, and all invariants are correct, so corrupted data can be still be seen as valid.

  • [removed] 9 days ago
    [deleted]
  • kaba0 9 days ago

    Yeah, and C is memory safe in absence of memory safety bugs..

marcus_holmes 9 days ago

> I absolutely hated this at the start, but now ~3 years into maintaining a go codebase, I find it quite nice

I've heard this so often. Thanks for sharing :)

I find going back to other languages and trying to read other people's code is a trial. There's always the temptation to write "smart" code that is terse but takes a lot of parsing to understand.

I love that I can pick up anyone's Go code and it's going to make sense almost immediately because everything is explicit, and laid out the same way (including that rhythm of "do the thing, test the error, do the thing, test the error")

tptacek 9 days ago

Go is memory-safe. It's not the definition of "memory-safe language" that it's impossible to write memory-unsafe code, only that ordinary code is memory-safe by default.

  • thinkharderdev 9 days ago

    > ordinary code is memory-safe by default

    What does that mean? What constitutes "ordinary"? I'm not sure there is any official definition of memory safety, but I would consider it to mean that aside from code that is explicitly marked as unsafe it is impossible to write code that has undefined behavior.

    • kccqzy 9 days ago

      Good definition. I've seen Go beginners trying to append to a slice from multiple goroutines. It works as well as calling push_back on the same vector from multiple threads in C++. It can easily corrupt GC state and lead to segfaults. The beginner didn't use any advanced trickery or the unsafe package. Therefore Go is not a memory safe language.

      • sshine 9 days ago

        > Therefore Go is not a memory safe language.

        Interesting.

        To quote the NSA [1], "Some examples of memory safe languages are Python, Java, C#, Go, Delphi/Object Pascal, Swift, Ruby, Rust, and Ada. Memory safe languages provide differing degrees of memory usage protections, so available code hardening defenses, such as compiler options, tool analysis, and operating system configurations, should be used for their protections as well."

        The narrow definition of memory safety here is:

        Go has garbage collection, so you won't have memory leaks or use-after-free.

        Go is powerful enough that beginners can cause segfaults by accidentally abusing internals, okay.

        I'm not sure this is a very redeeming property of Go: Being able to crash the GC, without the flexibility of manual memory management.

        But I'm not sure I'd categorize it as "not memory safe" for the same reason C/C++ aren't (a trade-off).

        Because I don't believe that you can generally leverage this for the kinds of memory exploits made in C/C++.

        I recall that some ML dialects (Standard ML and OCaml) have a library function Obj.magic : 'a -> 'b which escapes the type system. Using this can easily cause segfaults. Does that mean Standard ML and OCaml are not memory safe? Generally, no, they're extremely safe if you avoid that feature, which is most likely. This is arguably less safe than Go, since you most likely won't accidentally run that function.

        [1]: https://media.defense.gov/2022/Nov/10/2003112742/-1/-1/0/CSI...

      • Thaxll 9 days ago

        Appending from multiple goroutine to an in un-synchronized slice is "memory safe", it's completely different from c/c++.

        It behave exactly like Java or C# which are also memory safe.

      • everybodyknows 9 days ago

        > corrupt GC state

        I understand this to mean the runtime's internal state, not visible to user code. If so, in general we should expect almost any sort of crash mode to be possible. Seems fair enough to call this "memory-unsafe".

      • [removed] 7 days ago
        [deleted]
    • danielheath 9 days ago

      Go lets you use `unsafe.Pointer` (or indeed, assembly intrinsics) if you really want to, but those are certainly not used "ordinarily".

      • tsimionescu 9 days ago

        It's not just about that. Data races can expose an object in a state that was never written from any thread in Go, potentially corrupting even internal details not exposed. Simply writing a struct value from two different threads can expose this.

    • tptacek 9 days ago

      An example of extraordinary code would be code that interfaces with and/or pulls in non-memory-safe legacy C code.

      Another example would be code specifically contrived to highlight a soundness problem in the language.

      I used the term "extraordinary" to avoid exactly this kind of bickering over corner cases that aren't relevant to day-to-day software development (or at least, not in ways that aren't immediately evident when they come up.)

      • thinkharderdev 9 days ago

        > An example of extraordinary code would be code that interfaces with and/or pulls in non-memory-safe legacy C code.

        That's my point though. Of course calling non-memory safe native code over FFI can lead to memory-safety problems in any language. Likewise using the "unsafe" subset that basically every language has. But none of that is required in Go. It is only required that you mutate shared state from different threads, which is something that I would imagine happens in a lot of Go code codebases since it is an extremely easy mistake to make.

        To be clear I think:

        1. Go is mostly a memory safe language because it does in fact prevent the most common memory safety issues in C/C++ (UAF, buffer overflows, etc)

        2. It is LESS memory safe than other modern memory-sage languages (Rust, Java, C#, Python, etc....)

        3. The memory safety issues in Go are very difficult to exploit in code that is not specifically crafted to surface them

  • kaba0 9 days ago

    But ordinary go code is not memory safe. Data racing can trivially happen just by using the language's primitives. It requires no special keyword like unsafe, or native FFI like in other, actually memory safe languages (rust, or of the GCd kind, java, c#, Js)

    • kiitos 7 days ago

      You're using a definition of 'memory safety' which is not common.

  • remram 9 days ago

    How safe is it? It has pointers and they are widely used (more than Rust where pointers are unsafe, but there are other reference types). Are those safe?

    • dfawcus 9 days ago

      Generally.

      That is as it does not have pointer arithmetic, unlike C, and arrays / slices are bounds checked. So one will get a crash from a null pointer deref.

      The other risk with null pointer access is struct member access via such a pointer, but again due to lack of pointer arithmetic, that can't be easily triggered. The one way would be to have a massive struct, say much greater than the page size, and deref through that - fairly unlikely.

      The other reference types (slices, maps, interface values, channels) are safe unless subject to data race issues (multi goroutine update). However channels are safe there, as their role is to be used from multiple goroutines.

      So the path to lack of memory safety would be a data race, leading to type misinterpretation, hence type unsafety, then incorrect access and/or spatial and temporal unsafety as a consequence.

      Apart from poor design / implementation of explicit multi threaded apps, the most likely data race strikes me as accidental lexical capture by a goroutine, hence movement to the heap, and a resultant race. The sort of thing which was mentioned in a paper (by Uber?). Those should be amiable to detection by linters.

      The other case of races from poor threading design would be harder to automatically detect, but also harder to trigger. Probably avoidable by correct use of mutexes around access to the shared types (slices and maps), or simply by following an Actor or CSP design model.

      • remram 9 days ago

        Thanks for the summary, that is very helpful.

        At a language level though, it is either safe or unsafe. If it is "generally safe" provided you use it correctly, I would say it is not safe, in the strict sense.

        I don't think data races on pointers are allowed (looking at the memory model: https://go.dev/ref/mem) but I am not sure I have understood your scenario fully. Maybe I should read that paper you mention.

        Thanks again for the detailed response!

lynx23 9 days ago

> I know exactly what each function does

Isn't this basically the same argument that C people have been using since, what, 40 years?