skip to content

In cgo, what do C.CBytes and C.GoBytes do, and who frees the C copy?

level: juniorimportance: must knowfreq 45%

answer

  1. Two buffers, two owners
  2. One side mallocs, one side is collected
  3. Copy in, copy out, nothing shared
  4. The C heap has no collector watching it
  5. GoBytes needs a length; CBytes needs a free

basics

~10 s

C.CBytes copies a Go []byte into malloc'd C memory and returns an unsafe.Pointer you must release with C.free. C.GoBytes copies C memory back into a brand-new Go slice that the garbage collector owns.

solid answer

~40 s

Both are copy helpers that cgo generates, and copying is the point: after either call the two sides own separate memory. `C.CBytes(b)` mallocs `len(b)` bytes on the C heap, copies the slice contents in, and returns an `unsafe.Pointer`. Nothing in Go tracks that allocation, so you own it — `defer C.free(cbuf)`, with `#include <stdlib.h>` in the preamble. `C.GoBytes(p, C.int(n))` goes the other way: it allocates a Go slice, copies `n` bytes out of C memory into it, and hands you a normal garbage-collected `[]byte`, so the C side is free to release `p` immediately afterwards. Neither result aliases the other side. That safety costs a full copy in each direction, which is exactly why people reach for passing `&buf[0]` directly instead — and why cgo's pointer-passing rules exist.

code

go · 7 lines
go
// buf holds one chunk of the stream.
cbuf := C.CBytes(buf) // malloc'd copy on the C heap
defer C.free(cbuf)    // the preamble must #include <stdlib.h>

// ... the C codec reads cbuf and rewrites it, reporting n bytes ...

out := C.GoBytes(cbuf, C.int(n)) // a fresh, GC-owned Go slice

go deeper

for a junior

Be ready to say which direction each helper copies and to name C.free as the release for the C-heap allocation. Show the defer on the line after the allocation.

for a middle

Explain that both helpers copy, so neither result aliases the other side, and that the C allocation is invisible to Go's garbage collector. Mention that C.GoBytes needs an explicit length as a C.int.

for a senior

Demonstrate the cost judgment: know that each helper is an allocation plus a memcpy, be able to say what a leak here looks like in production (RSS grows, Go heap profile flat), and say when you would move to a direct pointer pass instead.

for a principal

Own the default for the codebase. Copying is the safe boundary policy that lets any reviewer approve a cgo call; carving out a zero-copy path is a deliberate exception that needs a benchmark behind it and a comment at the call site.

## What the two helpers are `C.CBytes` and `C.GoBytes` are not functions in any package you can import. They are part of the `C` pseudo-package that cgo synthesises for a file that imports `"C"`; cgo rewrites the call into real code at build time. Their whole job is to move bytes **by copying**, so that after the call each side owns memory the other side does not. Their shapes: - `C.CBytes(b []byte) unsafe.Pointer` — allocates `len(b)` bytes with C's allocator, copies the slice's contents into it, and returns a pointer to the allocation. The result is **not** NUL-terminated; it is raw bytes, not a C string. - `C.GoBytes(p unsafe.Pointer, n C.int) []byte` — allocates a new Go slice of length `n`, copies `n` bytes starting at `p` into it, and returns it. ## Who owns what This is the part interviewers actually probe, because it is where leaks and use-after-free come from. The pointer from `C.CBytes` lives on the **C heap**. Go's garbage collector does not know it exists, will never scan it, and will never reclaim it. If you drop the pointer, the memory is leaked for the life of the process. The only release is a call to C's `free`, which cgo exposes as `C.free` once the preamble includes `<stdlib.h>`: ```go cbuf := C.CBytes(chunk) defer C.free(cbuf) ``` Putting the `defer` on the line after the allocation is the habit worth building, exactly as you would with a file handle. In a streaming pipeline that does this per chunk, a missing `free` shows up as a heap that grows without any Go allocation to blame for it — a Go heap profile will look innocent, because the bytes are not on the Go heap at all. The slice from `C.GoBytes` is ordinary **Go memory**. It is collected like any other slice, it can be appended to, sliced, stored in a struct, and returned to a caller. Because it is a copy, the C side may free or overwrite its own buffer the instant `C.GoBytes` returns. That is the useful property: `C.GoBytes` is how you get out of C's lifetime rules entirely. ## Why copying at all Go memory and C memory are governed by different collectors — one automatic, one manual — and cgo's pointer-passing rules restrict what may cross the boundary and for how long. Copying sidesteps every one of those restrictions. A copied buffer cannot be invalidated by a garbage collection, cannot be moved out from under C, and cannot leave a stale Go pointer parked in a C struct. For a first cgo call in a codebase, copying is almost always the right default: it is boring, and boring is what you want on a boundary where mistakes surface as silent corruption rather than a clean panic. The cost is real, though, and it is why the question comes up in a review. Each helper is an allocation plus an `O(n)` memcpy. A pipeline pushing 64 KiB chunks through a C codec at high rate pays that twice per chunk, plus the malloc/free churn. When a benchmark with `-benchmem` shows the copy dominating, the alternative is to pass a pointer into the Go buffer itself (`&buf[0]`), which is legal for a `[]byte` under cgo's rules provided the C side does not keep the pointer past the call. That is a deliberate trade of safety for throughput, not a free upgrade. ## Details that trip people up - **The length argument is `C.int`.** `C.GoBytes` takes the byte count as a `C.int`, so you must convert (`C.int(n)`) and you must have a count in the first place — there is no way for it to discover a length, since raw bytes have no terminator. - **`C.CBytes` does not NUL-terminate.** If the C function expects a NUL-terminated string, `C.CBytes` is the wrong helper. - **An empty slice is a trap.** `C.CBytes(nil)` still returns a pointer you should free, but taking `&b[0]` on an empty slice panics with an index-out-of-range — a different route to the same C pointer, with a different failure. - **`C.free` needs the include.** Without `#include <stdlib.h>` in the preamble comment, `C.free` does not exist and the build fails; the fix is one line, but the error message points at Go, not at C. - **Copies do not alias.** Mutating the Go slice returned by `C.GoBytes` never changes anything the C side can see, and vice versa. If you expected shared memory, you wanted a pointer pass, not a copy. A good short answer names the direction of each helper, says who frees, and then volunteers the cost — that last part is what separates someone who has run cgo in production from someone who has read the doc page.

  • What does the C side have to guarantee about its buffer after C.GoBytes returns?
    Nothing. `C.GoBytes` has already copied the bytes into a fresh Go slice, so the C code may free, reuse, or overwrite its buffer immediately. That is the whole reason to copy: the returned slice has no relationship to C's lifetime rules, and the garbage collector owns it from that point on.
  • Your pipeline's resident memory climbs but a Go heap profile stays flat. How does that fit these helpers?
    That is the signature of a missing `C.free` on a `C.CBytes` result. The allocation lives on the C heap, which the Go collector neither scans nor reports, so a heap profile shows nothing while RSS grows. Look for every `C.CBytes` (and every `C.malloc`) without a matching `defer C.free`, or check the process with a C-level allocator tool.
  • When would you stop copying and pass a pointer into the Go buffer instead?
    When a benchmark shows the two copies dominating — large chunks at high rate. Passing `&buf[0]` is legal because a `[]byte`'s backing array holds no Go pointers, but it binds you to cgo's rule that C must not retain the pointer after the call returns. It is a considered trade, and worth a comment at the call site.

saying these in an interview costs you the question

  • Assuming the garbage collector frees a C.CBytes result
  • Treating C.CBytes output as a NUL-terminated C string
  • Believing C.GoBytes gives a view onto the C buffer
  • Freeing the Go slice returned by C.GoBytes
  • Calling both helpers on a hot path without measuring the copy