skip to content

What does the special import "C" line do in a Go file, and what is the comment above it?

level: juniorimportance: should knowfreq 32%

answer

  1. not a package on disk
  2. the comment right above it matters
  3. no blank line is allowed there
  4. that comment is compiled as C
  5. C names appear under a C. prefix

basics

~20 s

import "C" is not a real package; it turns on cgo for that file. The comment directly above it, with no blank line between, is the preamble: C code that is compiled and exposed to Go as C.name.

solid answer

~50 s

`import "C"` is a pseudo-import, not a package on disk and nothing `go get` fetches. It tells the go command to run cgo on that file, and it makes a synthetic package `C` available whose members are the C declarations visible in the **preamble** — the comment block immediately above the import, with no blank line between them. Anything you put there (`#include <stdlib.h>`, a `static` helper function, a `#define`) is compiled by the C compiler, and each C type, function, variable and macro becomes `C.int`, `C.free`, `C.myHelper` and so on. Because it is not a real package, `C` cannot be aliased, cannot be imported by a different package, and its types do not cross package boundaries — you wrap them behind ordinary Go types. Building such a file needs a working C toolchain on the build machine.

code

go · 19 lines
go
package main

/*
#include <stdio.h>
#include <stdlib.h>

static void greet(const char *name) {
	printf("hello, %s\n", name);
}
*/
import "C"

import "unsafe"

func main() {
	cs := C.CString("world")
	defer C.free(unsafe.Pointer(cs))
	C.greet(cs)
}

go deeper

for a junior

Be ready to say that import "C" is a marker rather than a package, and to point at the comment directly above it as the C code being compiled. Knowing that no blank line may sit between them is the detail that gets checked.

for a middle

Explain the mechanics: the go command runs cgo, the preamble is compiled by a C compiler, and each C declaration surfaces as C.name. Be able to show why C.int(n) is required rather than an implicit conversion from Go's int.

for a senior

Show that you keep C. types inside one thin wrapper package and expose only plain Go types, because each package importing "C" gets its own incompatible C types. Mention that adding cgo changes what the build machine must provide.

for a principal

Frame it as a build-and-dependency decision, not a coding one: switching a package to cgo commits every consumer's build to a C toolchain and the headers it needs. Be prepared to argue when a pure-Go alternative is worth the porting cost instead.

## What `import "C"` actually is Go has no foreign-function-interface syntax. Instead, the toolchain ships a program called **cgo**, and the trigger that runs it is a single line in a source file: ```go import "C" ``` There is no package named `C` anywhere on disk, in the module cache, or in the standard library. The line is a marker. When the go command sees it while building a package, it hands that file to `cgo` (you can invoke it yourself as `go tool cgo`), which reads the C declarations you supplied, generates Go stubs and C glue, compiles the C parts with a C compiler, and links everything into the final binary. ## The preamble The C declarations come from the **preamble**: the comment immediately preceding `import "C"`, with **no blank line** between the end of the comment and the import statement. That adjacency is the whole rule — a blank line turns the comment into an ordinary Go doc comment, cgo never compiles it, and every `C.something` in the file becomes an undefined reference. ```go package codec /* #include <stdlib.h> #include "codec.h" static int codec_version_major(void) { return CODEC_VERSION >> 16; } */ import "C" ``` The preamble is real C, compiled as C. It can contain `#include` directives, macros, typedefs, and — commonly — small `static` helper functions that paper over things cgo cannot express directly, such as calling a variadic C function or dereferencing a macro. ## What the pseudo-package exposes After the preamble is compiled, everything it declares is reachable through the `C.` prefix: - **Types**: `C.int`, `C.char`, `C.long`, `C.size_t`, `C.double`, plus any typedef or struct you included. A C struct `struct codec_ctx` is `C.struct_codec_ctx`; a union becomes an opaque byte array; a field whose name collides with a Go keyword gains a leading underscore. - **Functions**: `C.free`, `C.codec_version_major`, and anything else declared in the headers you pulled in. - **Variables and macros**: an `enum` constant or a simple `#define` of a constant value is usable as `C.CODEC_OK`. Two conversion helpers are worth knowing immediately because you will reach for them in the first hour: `C.CString` copies a Go string into C-allocated memory and returns `*C.char`, and `C.GoString` copies a NUL-terminated `*C.char` back into a Go string. ## Numeric types do not convert implicitly Go's `int` is not C's `int`. On most platforms Go's is 64-bit while C's is 32-bit, and cgo will not silently bridge them — the conversion is explicit and it is a genuine conversion, not a cast: ```go n := 4096 C.codec_set_buffer(C.int(n)) goBack := int(C.codec_get_buffer()) ``` The same applies to `C.char` versus Go's `byte`, and to `C.size_t` versus `uint`. Forgetting this is the most common first compile error a newcomer hits. ## The errno form Any C call may be written in a two-value assignment. The second value is the C `errno` variable after the call, delivered as a Go `error`: ```go rc, err := C.codec_open(path) ``` This is only meaningful when the C function's own return value already told you it failed — `errno` is not cleared by a successful call, so reading it unconditionally produces stale, confusing errors. ## Boundaries of the pseudo-package Because `C` is synthetic, several ordinary Go moves do not work: - You cannot write `import c "C"` to alias it. - You cannot export a `C.int` or a `*C.char` from your package and expect another package to use it — each package that imports `"C"` gets its *own* `C` types, and they are not interchangeable. The idiomatic shape is a thin wrapper package whose public API is pure Go (`func Encode(s string) (string, error)`) with all `C.` traffic hidden inside. - Reflection over `C.` types is not something to rely on. ## What it costs to switch on Adding `import "C"` to a package changes the build, not just the code: the package now requires a C compiler at build time, and the resulting binary is no longer produced by the Go toolchain alone. That is a real decision, not a detail — which is why teams that want a C library often first ask whether a pure-Go equivalent exists. ## What an interviewer is listening for They want to hear that `C` is not a package, that the preamble is adjacency-sensitive, that the C names appear under a `C.` prefix, and that numeric conversions are explicit. A candidate who says "you `go get` the C package" or who cannot say where the C code physically lives has not used cgo.

  • Why must there be no blank line between the preamble comment and import "C"?
    Adjacency is how cgo identifies the preamble. With a blank line the comment is just a Go doc comment, its `#include` lines and helper functions are never compiled, and every `C.` reference in the file fails to resolve. There is no warning telling you the comment was demoted — you get undefined-name errors instead.
  • How do you pass a Go int variable to a C function that takes an int?
    Convert explicitly: `C.int(n)`. Go's `int` is typically 64-bit while C's `int` is usually 32-bit, and cgo performs no implicit numeric conversion. The same discipline applies coming back — wrap the result in `int(...)`, and use `C.long`, `C.size_t` or `C.char` when the C signature calls for them.
  • What does the two-result form rc, err := C.some_call() give you?
    Any C function call may be used in a multiple-assignment context; the second value is the C `errno` after the call, converted to a Go `error`. Only consult it when the C function's own return value already indicates failure, because `errno` is not reset on success and you would otherwise report a stale error from an earlier call.

The preamble is less like an import list and more like a header file glued to the top of your Go file: whatever it declares is what the C. prefix can reach.

saying these in an interview costs you the question

  • Says C is a real package fetched by go get
  • Puts a blank line between the preamble and import "C"
  • Assumes a Go int converts to C.int automatically
  • Expects C. types to be usable from another package
  • Thinks cgo needs no C compiler at build time
  • Calls the preamble a normal doc comment