V — Reference

Source: https://docs.vlang.io/

V

  • Created: 2019 by Alexander Medvednikov; first public release June 2019
  • Latest stable: V 0.5.1 (2026-03-09); previous: V 0.5.0 (2025-12-31)
  • Status: Pre-1.0; breaking changes still occur. Treat as “actively evolving” — pin a commit/version for production
  • Paradigms: Multi-paradigm (imperative + procedural + light functional + structs/methods)
  • Typing: Static, strongly typed, type inference, generics, sum types, interfaces (structural-ish)
  • Memory: Minimal tracing GC by default; opt-in -autofree (compiler-inserted free, ~90-100% coverage, WIP); -gc none for manual; -prealloc arena
  • Compilation: Source → C → native via gcc/clang/tcc/msvc; also direct AMD64/ARM64/WebAssembly backends; very fast (~400k LoC/sec on x64); self-hosting compiler bootstraps in <1s
  • Primary domains: CLI tools, game/graphics, web (Veb), GUI (UI), embedded, scientific
  • Official docs: https://docs.vlang.io/ · Source: https://github.com/vlang/v

At a glance

V’s pitch is “Go-like simplicity, C-like speed, Rust-like safety, in one tiny binary.” Designed by Alexander Medvednikov; community-driven via the V Foundation. Reality: pre-1.0 with a long history of contested marketing claims (autofree completeness, “memory safety by default,” compile speed). The compiler is genuinely fast and produces small binaries, the standard library is broad (web, ORM, GUI, crypto), and v ships everything in one ~1MB binary. New backends in 0.5.x (cleanc, ssa/amd64, ssa/arm64) are converging on a self-hosting non-C path.

Getting started

Install (no separate version manager — V is self-updating):

git clone --depth=1 https://github.com/vlang/v
cd v && make            # Linux/macOS
.\make.bat              # Windows (MSVC or gcc)
sudo ./v symlink        # put `v` on PATH
v up                    # update to latest
v version

Hello world (hello.v):

fn main() {
    println('Hello, V!')
}

Run: v run hello.v. Compile: v hello.v (produces hello binary).

Project layout (created by v new myapp or v init):

myapp/
├── v.mod              # module manifest
├── src/main.v         # entry point
└── tests/             # *_test.v files run by `v test .`

Build/package tool: v itself. v install <module> (VPM), v install --git https://..., v build-module ., v doc ., v fmt -w ., v vet ., v test .. The v.mod file declares deps. Hex-style central registry at https://vpm.vlang.io/.

REPL: v repl (line-oriented; limited — feeds into a main() and recompiles).

Basics

Types & literals:

  • Numeric: i8 i16 int(=i32) i64 isize and u8 u16 u32 u64 usize, f32 f64, rune (alias for i32), bool, string (immutable, UTF-8)
  • 0b10, 0o17, 0x2A literal forms; _ separator (1_000_000)
  • Strings: single-quoted 'hi' is the default; backtick \a`isrune;r’\n’raw;’x = {expr}consistently — old$var` form is deprecated)

Variables & scoping: Immutable by default with :=, mutable via mut:

x := 10                 // immutable
mut y := 20             // mutable; reassign with =
y = 30
const pi = 3.14         // module-level only

Block-scoped. No shadowing across scopes by default. Unused vars/imports are errors, not warnings.

Control flow: if/else/else if (also expression-form: x := if cond { 1 } else { 2 }). for is the only loop:

for i in 0 .. 10 { ... }       // exclusive range
for i, v in arr { ... }
for cond { ... }               // while
for { ... }                    // infinite

match for sum types / values, with exhaustiveness:

match x {
    1 { println('one') }
    2, 3 { println('two or three') }
    else { println('other') }
}

Functions: pub fn for exports. Multiple return values. Named results with ?/! for fallible:

pub fn divide(a int, b int) !int {
    if b == 0 { return error('div by zero') }
    return a / b
}

Strings: UTF-8, immutable. s.len is bytes, not runes; iterate runes via s.runes(). Slicing returns a substring view: s[1..4]. Concatenation with +. f-strings via '${expr:format}'.

Collections: Arrays [1, 2, 3] (typed []int), maps {'a': 1, 'b': 2} (map[string]int), and built-in []&Foo for slice-of-pointers. Array methods: .map(), .filter(), .any(), .all(), .sort() (mutating), .sorted() (non-mutating).

Intermediate

Type system depth:

  • Structs with field defaults, embedded structs (composition), pub mut/mut/pub/private visibility per field
  • Interfaces are structural — implementations are implicit; interface Stringer { str() string }
  • Sum types: type Token = Number | Word | Punct then match-as
  • Generics: fn map[T, U](s []T, f fn(T) U) []U with explicit instantiation when inference fails (map[int, string](xs, ...))
  • Option ?T and Result !T are distinct: ? = may be none, ! = may error. Propagate with ?/! postfix; default with or { ... }

Modules: Directory = module. module foo declaration. Imports via import vweb, import json, import x.json2. No relative imports.

Error handling: No exceptions. Functions returning !T (error-or-value) or ?T (option). Caller must handle with or { ... }, propagate with trailing !/?, or call .unwrap() (panic on error). panic('msg') for unrecoverable.

Concurrency: Two primitives:

  • spawn fn_call() → OS thread (thread T handle); .wait() to join
  • go fn_call() → V coroutine (lighter, where the async runtime is enabled)

Channels are Go-style: ch := chan int{cap: 10}, ch <- 5, v := <-ch, ch.close(). select { ... } for multi-channel. Shared mutable state requires shared keyword + lock x { ... } / rlock x { ... } blocks; only structs/arrays/maps may be shared.

I/O: os (files, env, args, exec), net, net.http, net.websocket, io, time, json, json2, crypto.*, regex (now with non-greedy quantifiers in 0.5.1), db.sqlite/db.pg/db.mysql, sync, runtime.

Stdlib highlights: Built-in ORM (compile-time SQL generation), built-in Veb web framework, gg/ui for graphics/GUI, vweb.assets, pico minimal HTTP, flag arg parser, cli framework, term terminal styling, toml/yaml/xml/csv parsers.

Advanced

Memory model: Default GC is a small mark-sweep collector (Boehm-style). [heap] attribute on structs forces heap allocation for refs that escape. [noinit] skips zero-init. Autofree is the controversial flagship — compiler analyzes lifetimes and inserts free() at scope exit; it claims 90-100% coverage with GC catching the rest. As of 0.5.x autofree is still labeled WIP and is not the default. -prealloc arena allocates one big buffer freed at exit (great for compilers, bad for long-running services).

Concurrency deep dive: V coroutines run on a Photon-derived (libphoton) green-thread runtime when enabled with -coroutines. go uses these; spawn always uses OS threads. Channels implement Go’s CSP semantics; select supports timeouts (> 100 * time.millisecond { ... }). Thread-safety is enforced syntactically: you cannot pass a non-shared mutable to another goroutine without the compiler erroring.

FFI: First-class C interop:

#include <math.h>
fn C.cos(f64) f64
 
fn main() { println(C.cos(0.5)) }

#flag -lfoo for linker flags. unsafe { ... } block required for raw pointer arithmetic. v translate file.c attempts C → V translation (limited). Inline assembly via asm amd64 { ... } blocks.

Reflection: Compile-time only via $for field in T.fields { ... }. Runtime type info is minimal — V favors compile-time codegen. typeof(x).name, typeof(x).idx for runtime tags on sum types.

Performance tools: v -prof program.v injects a sampling profiler. v -cflags -pg for gprof. v -d trace_gc for GC events. v -showcc prints the underlying C invocation for debugging.

God mode

Comptime metaprogramming:

  • $if linux { ... } $else $if windows { ... } for cond compilation
  • $for field in T.fields { println(field.name) } to walk struct fields at compile time
  • $compile_error('unsupported') to abort compilation with message
  • $tmpl('view.html') for compile-time template rendering used by Veb
  • $env('HOME') reads env var at compile time
  • $embed_file('asset.png') bakes a file into the binary

Source → C pipeline: Look at v -o out.c file.v to see the generated C. The 0.5+ cleanc backend produces dramatically more readable C and is now self-hosting (compiles V itself). The new ssa/amd64 and ssa/arm64 backends generate native code without going through C — compile times drop further but stdlib coverage lags the C path.

Hot reload: Mark a function [live] fn foo() and run with v -live run app.v. Recompiles + reloads on file change without restarting. Used heavily in V’s GUI/game projects.

v translate: Bundled converter that translates C (and historically attempted Go/JS) to V. Quality varies — useful for porting headers, not whole projects.

Cross-compilation out of the box: v -os windows hello.v, -os linux, -os macos, -arch arm64. No toolchain hunting — V bundles tcc as a fallback C compiler.

Inline C:

#include <stdio.h>
fn main() {
    a := 5
    C.printf(c'a = %d\n', a)
}

The c'...' literal is a C string (&u8).

The “memory-safe-by-default” claim — what it actually means: V prevents some common C bugs (no manual malloc/free in safe code, bounds checks on arrays, no implicit nullability), but does NOT have Rust-style borrow checking. Aliased mutable references are allowed; data races are caught only across go/spawn boundaries via shared. Autofree’s coverage claims have been disputed since 2019. Treat V as “much safer than C, much less rigorous than Rust.”

Idioms & style

  • Naming: snake_case for vars/fns/files, PascalCase for types/enum variants, SCREAMING_SNAKE for consts
  • Formatter: v fmt -w . is mandatory — there is exactly one official style and CI rejects unformatted code
  • Linter: v vet ., v missdoc . (missing docs), v check-md . (markdown code blocks)
  • One-style philosophy: Like Go — no debate, vfmt decides. mut only when needed. Errors handled at the call site, not at the throw site. Avoid unsafe blocks in app code
  • Expert review focus: Watch for misuse of unsafe { } (raw pointer ops), missed or { } on ?/! returns, [heap] annotations forced by aliasing patterns, misuse of shared outside structs/arrays/maps, and assumptions about autofree that don’t hold without -autofree

Ecosystem

DomainLibrary
WebVeb (built-in, was vweb), pico (minimal), valval
GUIvlang/ui (cross-platform), gg (2D graphics, ImGui-style), iui
Gamevsdl2, raylib bindings, the Volt game engine
DatabaseBuilt-in ORM (sqlite/pg/mysql), vsql
HTTP clientnet.http (stdlib), vesper
CLIflag and cli (stdlib)
TestBuilt-in _test.v files, v test .
DocsBuilt-in v doc, hosted on https://modules.vlang.io/

Notable users: Volt (Discord-like client written in V), VEX (web framework), Verdek (orchestration), Coachonko (CMS). V’s own self-hosting toolchain (compiler + vfmt + vls) is by far the largest V codebase.

Gotchas

  • Pre-1.0: Breaking changes between minor releases. The 0.5 series introduced the ${var} interpolation form; old $var is removed
  • Autofree is opt-in and WIP — default is GC. Don’t believe blog posts claiming “no GC by default”
  • Compile-speed claims are best-case — measured against the C-backend with tcc; ssa backends are still maturing; first compile of stdlib is much slower than incremental
  • spawn uses OS threads, not goroutines — high-concurrency code wants go + -coroutines
  • s.len is byte length, not rune count — use s.runes().len for char count
  • Unused imports/vars are hard errors// vfmt off won’t help; remove them
  • Sum-type matching must be exhaustive — and match requires else for non-sum-type values
  • Generics inference is best-effort — pre-0.5.1 cases sometimes need explicit [T] instantiation; the new -new-generic-solver improves this
  • C backend leaks identifiers — debugging stack traces show V-mangled C names. Use -cstrict to catch warnings as errors
  • The & (reference) and * (deref) operators look like C but allocate via [heap] rules — references that escape force heap allocation
  • v translate for non-C inputs is largely abandoned — Go/JS translation works on toy programs only

Citations