Better to structure your code so that you do the pointer manipulation (and allocation) in Go code instead, and leave assembly only for what is absolutely necessary for performance (usually things like bulk operations, special instructions, and so on).
While I generally agree with this, one way to mitigate the maintenance issue is to offer a macro assembler instruction that performs a write barrier that is kept up to date with what the Go compiler emits. If it the compiler itself uses that macro assembler method, it's already pretty easy.
After many years of racking my brain on how to make each of the things "blessed and bulletproof", I've realized that systems languages will inevitably be used by people building high performance systems. People doing this will go to crazy lengths and low-level hackery that you can't anticipate, e.g. generating their own custom machine code[1], doing funky memory mapping tricks, etc. The boundaries between that machine code and your language are always going to run into representational issues like write barriers and object layouts.
[1] Pretty much all I do is language VMs, and there is no way around this. It will have a JIT compiler.
https://go.dev/wiki/AssemblyPolicy
https://github.com/golang/go/tree/master/src/crypto/internal...
For example a RISC-V CPU implementing the Zkt extension is required to implement a whole bunch of logical and arithmetic integer operations with operand-independent timing. This includes the two "branchless move" instructions from the Zicond extension, czero.eqz and czero.nez.
For all my complaints about Go's design, that is certainly one that I appreciate.
True.
> so it needs to have an assembler for its compiler.
No, it doesn't need an assembler for this. As you said correctly, it compiles to machine code directly.
While it was once fairly common to use assembly as an intermediate step, very few or any modern compilers do that. They just compile directly to binary machine code.
Go does have a -S flag to generate assembly language so you can review the generated code more easily. But that assembly code isn't part of the compilation pipeline, it's just optional output for human review.
When dealing with purely high-level code (including C without inline assembly), the compiler doesn't need a discrete assembler. This much is true, and most modern compilers will not spit out assembly unless requested.
However, there's usually still a stage of the compilation process where an assembly-like internal representation is used. Once the compiler has chosen which registers to use and where, and which instructions to use and in what order, etc., it's close to machine code but not fully there yet. At this point, jump targets will still be labels without concrete addresses, instructions will be still be referenced by mnemonic, etc.
Serializing this internal representation as assembly will generally produce more readable code than disassembling the final binary. So it's not assembly exactly, but it's pretty close.
Interestingly, Go also has a -d=ssa/all switch that outputs not just an assembly representation of the final code, but also the results of each optimization pass.
Here is a discussion I had with ChatGPT about this:
https://chatgpt.com/share/6859aea5-df1c-8012-be70-f2361060fb...