The V Programming Language: A Comprehensive Textbook Guide
Welcome to the ultimate learning guide for the V programming language! This textbook is structured specifically to take you from a complete beginner (zero programming experience) to an advanced V developer capable of building high-performance, concurrent, and safe systems applications. Rather than treating V as a list of syntax rules, this guide emphasizes a practical path: learn the core ideas, run the examples, and build small projects as you go.
How to read this book: Each section starts with a clear explanation of a fundamental programming concept, followed by concrete V code examples. Every example contains the exact code from the repository, formatted in clean code blocks so you can easily copy and run them yourself.
Interactive Learning: You can test any code example from this guide live in your browser using the V Playground.
Never Programmed Before? Start Here (In Plain English)
If you have never written a line of computer code beforeโor if big technical words usually make programming guides feel confusingโyou are in the right place. Every chapter and lesson in this guide is written so you can understand it using normal, everyday language.
Here are the 5 big ideas of all computer programming, explained in plain words:
- A Program Is Just a Recipe: A computer is like a very fast, very literal helper in a kitchen. A program (
.vfile) is simply a text file where you write step-by-step instructions from top to bottom. Every V program starts at a main step calledfn main() { ... }. - Variables Are Labeled Storage Boxes: When your program needs to remember something (like a player's name or a price), you put it in a variable using
:=(for example,name := 'Sam'). In V, boxes are locked by default (immutable) so you don't change them by mistake. If you want a box whose value can change later, putmut(mutable = changeable) in front of it:mut score := 0. - Data Types Are Box Shapes: Computers like to know what kind of thing is inside each box:
string= text inside quotes ('Hello')int= a whole number (42)f64= a decimal number (3.14)bool= a Yes/No switch (trueorfalse)[]int(array) = a numbered list of items ([10, 20, 30])map[string]int(map) = a lookup dictionary ({'apples': 5})struct= your own custom form that bundles several boxes together (like aUserwith anameandage).
- Control Flow Is Making Choices and Repeating Work:
- Use
if/elseormatchwhen your program needs to make a decision ("If the score is over 100, print You Win!"). - Use
forwhen your program needs to repeat a step ("For every name in the list, print a greeting").
- Functions Are Reusable Mini-Recipes: When you have a useful set of steps you want to use more than once, wrap them in a function (
fn). You feed inputs into(...)and it hands back an answer withreturn. And if a function might run into a problem (like a missing file), V uses?or!and a simpleor { ... }safety net so your app never crashes unexpectedly.
Repository Structure
This book is paired with a topic-based repository layout so it is easier to explore examples by concept. The structure is intentionally arranged in a learning sequence rather than as a flat list of files:
variables_and_constants/,primitive_types/, andcontrol_flow/for the foundation of the languagefunctions/andstructs/for building reusable programs and modeling dataerror_handling/,modules/, andtesting/for reliability and organizationconcurrency/,channels/,json_and_orm/,sqlite/, andnotes_api/for real-world applicationslanguage_updates_and_stdlib/for newer language features and grouped standard library examples that are easier to browse by topic
Following this structure makes it simpler to move from small examples to larger projects, and it also gives contributors a clear place to add new lessons.
A better way to think about the repo
- Start with the introductory folders when you are learning V for the first time.
- Use the middle sections when you are ready to write more structured programs.
- Explore the application-oriented folders once you want to build something practical.
Contributing new content
When adding a new lesson, keep it in the most relevant topic folder and use a numbered naming pattern such as 01_topic_name/ so the learning flow stays predictable.
Quick Start: Learn V by Building Things
If you are new to programming, the fastest way to learn V is to start small and build something real. Follow this sequence:
1. Compile V from Source (Official Method)
Always compile V directly from source. Installing V via third-party package managers like Homebrew is not officially supported by the creators of V and frequently causes outdated builds and broken standard library resolution. Compiling from source is fast and guarantees you have the latest stable features.
# Clone the repository
git clone https://github.com/vlang/v
cd v
# Compile V compiler
make
# Create a global symlink so `v` is available system-wide
sudo ./v symlinkVerify your installation:
v versionv-analyzer Configuration: If you use VSCode or the Antigravity IDE with the v-analyzer language server, set custom_vroot in your settings to your cloned V repository path (e.g. ~/v or /path/to/v). This allows v-analyzer to properly index the vlib standard library and provide code completion and hover documentation.
2. Create Your First Program
Create a file named hello.v:
fn main() {
println('Hello, V!')
}3. Run and Build
- Run immediately:
v run hello.v - Build a native binary:
v -o hello hello.v
V has a few ideas that are worth remembering early:
- Variables are immutable by default, so use
mutwhen you need to change a value. - Modules help organize larger programs.
optionandresultmake error handling explicit.spawnand channels make concurrency approachable.
Core Language Essentials to Learn Early
A beginner-friendly roadmap becomes much clearer when you call out the core concepts that show up again and again in V programs:
mutand immutable-by-default variables- functions, parameters, and return values
- structs and methods for modeling data
enumandmatchfor branching on discrete choices- modules and imports for organizing code
?and!for option and result typesdeferfor cleanup work before a function exitsunsafeonly when you truly need low-level access- type aliases and sum types once you are comfortable with structs and enums
These ideas form the backbone of most V programs, so it is worth learning them in a small, practical order rather than trying to memorize every feature at once.
Must-Learn-Before-Building Checklist
Before you start a larger project, make sure you can comfortably do the following:
- write a small program with a
mainfunction - declare variables and explain when to use
mut - define and call functions with clear parameters and return types
- model simple data with structs
- choose between
if,match, andforfor control flow - split code into modules and import them correctly
- handle optional or failing values with
?and! - use
deferfor cleanup work when needed - avoid
unsafeunless you truly need it
If you can do these reliably, you are ready to move from tiny examples to real programs.
Why This Matters
The goal is not just to memorize syntax. Each concept in this guide solves a real programming problem:
- Variables and mutability help your program store and update information safely.
- Functions let you break a program into small, reusable pieces.
- Structs help you model real-world data such as users, files, or payments.
- Error handling makes programs more predictable and easier to debug.
- Concurrency helps programs do more work efficiently when tasks can run independently.
When you learn a new feature, ask yourself: โWhat problem does this solve?โ and โHow would I use it in a small program?โ
Suggested Learning Path
A beginner-friendly path through this guide is:
- Start with Chapters 1-4 to learn the basic syntax and control flow.
- Move into functions, structs, and modules to structure your programs.
- Practice with tests and error handling before tackling larger projects.
- Finish with concurrency, JSON, and databases by building a small app.
Next-Level Language Features to Explore
Once the basics feel natural, the next step is to expand your comfort with a few higher-level features:
- type aliases for clearer naming
- sum types for values that can be one of several shapes
- generics for reusable data structures and helpers
- interfaces for shared behavior across different types
- higher-order functions and function values
unsafeand pointers only when you need low-level performance or interop
These features are not required for every beginner project, but they become very useful once you start writing more structured or reusable code.
Mini Projects to Try
These projects will make the guide feel much more practical:
- A command-line to-do list
- A number guessing game
- A simple file organizer or text search tool
- A notes app that stores data in JSON or SQLite
- A cross-platform GPU desktop app (macOS, Linux, Windows) using V's
ggengine: simple_gg - A macOS desktop app with a native Cocoa UI & Visual Designer: vlang_simplegui
- A cross-platform Visual RAD Studio & desktop IDE with Webview: vlang_webview_rad_studio
- 88+ playable 2D/3D arcade games in V with sprites & WAV audio: vlang_sdl2_games
- 88 playable arcade games in V with 100% procedural graphics & sound: sdl_games
- A macOS desktop app with a webview-based UI: vlang_macos_webview_app_template
Practice Exercises
Try these small exercises as you move through the guide:
- Write a program that prints your name and age.
- Create a function that adds two numbers and returns the result.
- Build a tiny program that stores a user in a struct and prints the fields.
- Write a loop that prints the first 10 even numbers.
- Use an
optionorresultin a small helper function and handle the failure case.
If you get stuck, write the smallest possible version first and test it before adding more features.
Your First Project: A Tiny CLI Greeting App
A great first project is a small command-line app that asks for a name and prints a greeting. This lets you practice variables, functions, input, and output without getting overwhelmed.
Step 1: Start with a simple main function
fn main() {
println('Hello, V!')
}Step 2: Add a name variable
fn main() {
name := 'Ada'
println('Hello, ' + name + '!')
}Step 3: Make it interactive
import os
fn main() {
name := os.input('What is your name? ')
println('Hello, ' + name + '!')
}Step 4: Improve it with a function
import os
fn greet(name string) string {
return 'Hello, ' + name + '!'
}
fn main() {
name := os.input('What is your name? ')
println(greet(name))
}Why this project is useful
This project teaches the core flow of programming in V:
- write a small program
- test it
- add a feature
- refactor it into functions
- make the output clearer
Once this feels easy, you can extend it with a command-line option, a loop, or a saved history file.
What Most Programmers Want Next
A strong guide should help readers move from learning syntax to building and debugging real software. The following topics are especially useful for most programmers:
Quick Reference
- Run a file:
v run hello.v - Build an executable:
v -o hello hello.v - Use
mutwhen a value needs to change - Use functions to keep logic organized
- Use modules to split larger projects into manageable files
- Use tests to verify behavior as you grow your program
Common Beginner Mistakes
- Forgetting to make a variable
mutbefore changing it - Mixing up declaration and assignment
- Writing code without small, testable functions
- Trying to learn too many concepts at once instead of building one small feature
Debugging and Reading Errors
When something fails, focus on the first compiler error, reduce the problem to a smaller example, and test one change at a time. This is often faster than changing many lines at once.
Real-World Workflow
As your projects grow, you will want to know how to:
- split code into modules
- write tests
- structure folders clearly
- read documentation and standard library examples
- move from small scripts to larger applications
From Practice to Real Projects
Once the basics feel comfortable, the next step is to build small applications that combine multiple ideas. A very effective progression is:
- Build a tiny CLI tool that reads input and prints output.
- Add functions, structs, and tests.
- Introduce modules so the code is easier to maintain.
- Add file I/O or JSON handling for persistence.
- Explore concurrency for tasks that can run in parallel.
This progression helps learners move from โI can read examplesโ to โI can build useful software.โ
Where to Go Next
After finishing this guide, the best next steps are:
- read the official V documentation and examples
- try the V Playground for rapid experimentation
- build one project end to end instead of reading only
- contribute to or study real V repositories for idiomatic patterns
Real-World V Projects & Ecosystem Showcase
Want to see what you can build once you learn V? Check out these five complete, open-source desktop, GUI, and game projects built in V by @codecaine-zz:
| Project | What You Can Build / Explore (In Plain English) | Key Technologies |
|---|---|---|
| simple_gg | Cross-Platform GPU Desktop Apps: A lightweight, beginner-friendly UI framework for building hardware-accelerated desktop apps across macOS, Linux, and Windows with 260+ themes, shaders, 47 studio apps, and 45 utility modules. | V gg (Sokol GPU), Cross-Platform UI |
| vlang_simplegui | Native macOS Desktop Apps & Visual Designer: Build real macOS Cocoa desktop apps in V with 115+ native controls, 50+ themes, 140+ demos, and a built-in drag-and-drop Visual UI Designer (like Delphi or Visual Basic) โ no Objective-C needed! | V + macOS Cocoa, Visual RAD Designer |
| vlang_webview_rad_studio | Cross-Platform Visual RAD Studio & IDE: A full visual drag-and-drop Rapid Application Development (RAD) Studio powered by V and native Webview (macOS WebKit, Windows WebView2, Linux WebKit2GTK) with 70+ components and 76 themes. | V + Webview, Visual Form Designer |
| vlang_sdl2_games | 88+ Arcade Games (Sprite & Real Audio Edition): A massive collection of 88+ playable 2D and 3D arcade games, retro classics, puzzle adventures, and physics simulations in V featuring high-res PNG sprite sheets, WAV sound effects, music, and F5/F9 quick save states. | V + sdl (SDL2), Sprites & WAV Audio |
| sdl_games | 88 Arcade Games (Zero-Asset Procedural Edition): All 88 playable arcade and retro games built in V with zero external image or audio filesโ100% math-generated sound effects (PCM synthesis), vector graphics, and built-in pixel fonts! | V + sdl, Procedural Audio & Graphics |
Beginner Project Roadmap
A practical roadmap for new V developers could look like this:
Milestone 1: CLI App
Build a small command-line app that accepts user input and prints useful output. This helps you practice functions, variables, and control flow.
Milestone 2: Data App
Add file I/O or JSON handling so your app can save and load data. This introduces practical patterns for real-world applications.
Milestone 3: Structured App
Split the app into modules and add structs for data models. This is where programs become easier to maintain.
Milestone 4: Tested App
Write tests and improve reliability. This is an important step for building confidence as a programmer.
Milestone 5: Concurrent App
Explore concurrency with spawn and channels for tasks that can run in parallel. This is where V becomes especially compelling for performance-oriented software.
Setup Checklist
Before you start coding in V, make sure you have:
- V installed and available on your terminal
- a text editor or IDE with basic syntax highlighting
- a way to run and test small programs quickly
- a folder for practice files and mini projects
Quick Glossary: Plain-English Dictionary & Cheat Sheet
Whenever you run into a programming word or V symbol you aren't sure about, check these two plain-English tables!
1. Programming Words Translated into Everyday English
| Word / Symbol | What It Means in Plain Words | Everyday Analogy | V Code Example |
|---|---|---|---|
Variable (:=) | A named storage box that holds a value. | A labeled plastic bin on a shelf. | age := 25 |
| Immutable | Lockedโonce set, the value inside cannot be changed. (Default in V!) | Writing on a label in permanent marker. | name := 'Ada' |
Mutable (mut) | Changeableโunlocked so you can update its value later with =. | Writing on a whiteboard you can erase and rewrite. | mut score := 0 then score = 10 |
Constant (const) | A permanent app-wide value declared at the top of a file. | The speed of light or the number of days in a week. | const max_users = 100 |
String (string) | Any piece of text wrapped in single quotes. | A sentence on a note card. | msg := 'Hello, ${name}!' |
Interpolation (${...}) | Plugging a variable's value directly inside a text string. | A fill-in-the-blank form letter. | 'Score: ${score}' |
Integer (int) | A whole number (positive, negative, or zero) with no decimals. | Counting whole apples (1, 2, 3). | items := 12 |
Float (f64) | A number with a decimal point. | Money or measurements (19.99, 3.14). | price := 19.99 |
Boolean (bool) | A simple Yes (true) or No (false) value. | A light switch that is either ON or OFF. | is_ready := true |
Rune (rune) | A single character, symbol, or emoji wrapped in backticks. | One single letter tile in Scrabble. | `` |
Array ([]Type) | An ordered, numbered list of items of the same type (starts at slot 0). | A numbered shopping list. | mut nums := [10, 20, 30] |
Append (<<) | Pushes a new item onto the end of a changeable (mut) array. | Adding one more item to the bottom of a list. | nums << 40 |
Slice (a[1..3]) | A window viewing a smaller range of items inside an array. | Looking at pages 1 to 2 of a notebook. | part := nums[0..2] |
Map (map[K]V) | A lookup dictionary that pairs unique keys with values. | A phonebook where you look up a name to get a number. | mut ages := {'Sam': 30} |
Function (fn) | A named, reusable mini-recipe that can take inputs and return an answer. | A blender: put fruit in, get a smoothie back. | fn add(a int, b int) int { return a + b } |
Struct (struct) | A custom blueprint that bundles related variables (fields) together. | A contact card with boxes for Name, Email, and Phone. | struct User { name string age int } |
| Method | A function attached directly to a struct so you can call item.action(). | Teaching a Dog struct how to dog.bark(). | fn (u User) greet() { ... } |
Option (?Type) | Means a function might return a value OR none (nothing found). | Checking a mailboxโthere might be a letter or nothing (none). | fn find(id int) ?User |
Result (!Type) | Means a function might succeed OR fail with an error('message'). | Trying to open a file that might be locked or missing. | fn load(path string) !string |
or { ... } Block | A safety net placed right after a ? or ! call to handle none or errors. | A backup plan if the main plan fails. | data := os.read_file('a.txt') or { '' } |
defer | Runs a cleanup block automatically right before the current function exits. | Turning off the kitchen lights automatically when you leave. | defer { file.close() } |
Module (module) | A folder of .v files organized around one topic. | Separate drawers in a toolbox. | import os / pub fn helper() |
assert | Checks that a condition is true during a test; stops and explains if false. | An inspector checking that $2 + 2 = 4$. | assert add(2, 3) == 5 |
spawn | Runs a function in the background at the same time as your main code. | Hiring a second helper to chop veggies while you boil water. | spawn download_file(url) |
Channel (chan) | A safe pipe (<-) for passing messages between background spawn workers. | A conveyor belt between two assembly workers. | ch <- 42 and val := <-ch |
| JSON | Universal text format ({"key": "value"}) used to save or send structured data. | A standard shipping label any computer can read. | json.encode(user) |
| ORM / SQLite | Storing and searching structured data inside a database table using V code. | An automatic filing cabinet you can search instantly. | sql db { select from User } |
2. "How Do I...?" Everyday V Cheat Sheet
| I Want To... | Write This in V | Where to Learn More |
|---|---|---|
| Print a message to the screen | println('Hello, world!') | Chapter 1 |
| Save a value that never changes | name := 'Alice' | Chapter 2 |
| Save a value that can change later | mut count := 0 then count += 1 | Chapter 2 |
| Put a variable inside text | println('Hello ${name}, count is ${count}') | Chapter 3 |
Make a decision (if / else) | if count > 10 { println('Big') } else { println('Small') } | Chapter 4 |
| Repeat something 5 times | for i in 0 .. 5 { println(i) } | Chapter 4 |
| Create a list and add an item | mut items := ['a', 'b'] then items << 'c' | Chapter 5 |
| Loop through every item in a list | for item in items { println(item) } | Chapter 4 |
| Filter or transform a list in 1 line | evens := nums.filter(it % 2 == 0) / doubled := nums.map(it * 2) | Chapter 5 |
| Create a key-value lookup map | mut scores := {'Ada': 95} then scores['Bob'] = 88 | Chapter 5 |
| Write a reusable function | fn double(n int) int { return n * 2 } | Chapter 6 |
| Group data into a custom type | struct Book { title string pages int } | Chapter 7 |
| Safely handle a function that can fail | text := os.read_file('notes.txt') or { 'default text' } | Chapter 8 |
| Run a task in the background | t := spawn heavy_work() then t.wait() | Chapter 11 |
| Convert a struct to/from JSON | raw := json.encode(book) / b := json.decode(Book, raw)! | Chapter 12 |
How to Use This Guide Effectively
To get the most from this book:
- Read the explanation first, but do not stop there.
- Run each example locally.
- Change one small thing and observe what happens.
- Write your own tiny version before moving on.
- Apply each idea in a small project as soon as possible.
Table of Contents
- Chapter 1: Getting Started with V
- Under the Hood: The V Compilation Pipeline & Architecture
- Code Comments
- Chapter 2: Variables and Constants
- Under the Hood: Immutability, Memory Layout & Constant Inlining
- Constants
- Variables
- Chapter 3: Primitive Data Types
- Under the Hood: Primitive Data Representation, Strings & UTF-8
- Primitive Types Demo
- Boolean Type
- Numeric Types
- Rune Type
- String Type
- Chapter 4: Control Flow
- Under the Hood: Control Flow Optimization, Jump Tables & Defer Stack
- Control Flow Extras
- Chapter 5: Collections: Arrays and Maps
- Under the Hood: Dynamic Arrays, Slicing & Hash Table Mechanics
- Arrays
- Maps
- Chapter 6: Functions
- Under the Hood: Calling Conventions, Multiple Returns & Closures
- Advanced Function Features
- Function Extras
- Chapter 7: Structs (Custom Types)
- Under the Hood: Struct Alignment, Method Dispatch & Heap Allocation
- Struct Basics & Fields
- Chapter 8: Error Handling
- Under the Hood: Option (?T) and Result (!T) Representation
- Option & Result Types
- Chapter 9: Organizing Code with Modules
- Under the Hood: Module Resolution, Namespaces & VPM
- Modules & Project Structure
- Installing External Packages
- Chapter 10: Writing Tests in V
- Under the Hood: Test Discovery & The Assertion Engine
- Assertions & Unit Testing
- Chapter 11: Concurrency and Channels
- Under the Hood: V-Routines, Thread Pooling & Channel Ring Buffers
- Channels & Communication
- V-Routines & Concurrency
- Chapter 12: Working with Databases and JSON
- Under the Hood: Compile-Time Reflection & Type-Safe ORM
- Case Study: Notes API
- JSON & ORM
- SQLite Integration
- SQLite CRUD Helper
- Sqlite Raw Crud
- Chapter 13: Standard Library & Advanced Features
- Under the Hood: Sum Types, Generics, Memory Models & C Interop
- Inline Assembly & C Interop
- Networking (TCP, UDP, SSL, WebSockets)
- Other Stdlib Updates
- Strings.Lorem Helper
- WebAssembly Compilation
- Chapter 14: Useful Boilerplates and Application Templates
- CLI Command-Line Application Boilerplate
- REST API Server Boilerplate
- Worker Pool Concurrency Boilerplate
- OS and File Utilities Boilerplate
- String Utilities Boilerplate
- Math and Statistics Boilerplate
- Array Utilities Boilerplate
- Chapter 15: Comprehensive Practice Exercises
- Practice Exercises Overview
Chapter 1 Getting Started with V
Plain-English Chapter Guide (Start Here if You Are New to Coding!)
- The Big Idea in Simple Words: A computer program is just a list of instructions written in a text fileโlike a cooking recipe. The V compiler is a helper tool that reads your recipe, checks for mistakes, and turns it into a fast app your computer can run. Comments (
//and/* ... */) are sticky notes you leave inside your code for yourself or other humans; the computer completely ignores them. - Words to Know:
- Source Code (
.vfile): The plain text file where you write your V instructions. - Compiler (
v): The tool that translates your V text file into a runnable program. - Comment (
//): A note for humans that the computer skips over when running the program. - Console / Terminal: The text window where your program prints messages using
println(...).
Quick Access โ Jump to any lesson in this chapter
Below is an index of all code examples in this chapter. You can use these links to jump directly to any specific code example:
Code Comments
This chapter introduces the core design philosophies of V. You will learn how to set up your development environment, compile and run programs, and document your code using comments.
Under the Hood: The V Compilation Pipeline & Architecture
๐ง Optional Deep Dive โ Click to Expand / Collapse
Plain-English Summary: In simple words: V reads your code in one quick pass, checks that your types and variables are safe, and translates it into super-fast machine code in less than a secondโwith zero bulky background software required.
Understanding how V compiles and executes your code is fundamental to writing high-performance, idiomatic applications. Unlike languages that rely on virtual machines or heavy runtime interpreters, V is a native compiled language with a uniquely fast and transparent pipeline.
The 5 Stages of the V Compiler Pipeline
- Single-Pass Parsing & AST Generation:
The V compiler reads source files in a single pass, constructing a streamlined Abstract Syntax Tree (AST). V deliberately omits complex macro expansion phases or runtime template interpretation, allowing AST construction to finish in milliseconds.
- Static Type Checking & Safety Analysis:
The compiler enforces V's core safety invariants: immutability by default, absence of unhandled null values, no variable shadowing, and bounds checking on array accesses. Type checking and compile-time reflection are resolved entirely during this pass.
- C99 Source Code Emission:
Rather than inventing an unoptimized intermediate machine-code generator, V transforms the AST directly into clean, human-readable C99 source code. This design grants V immediate compatibility with existing C libraries, debuggers (GDB, LLDB), and CPU architectures.
- Native C Compiler Execution:
V invokes a backend C compiler to produce the final machine code:
- TCC (Tiny C Compiler): Used by default in development mode. TCC compiles C code in under 0.1 seconds, delivering near-instant
v runfeedback loops. - Clang / GCC: Used when building release binaries (
v -prod). Clang/GCC apply advanced optimization passes, Link-Time Optimization (LTO), and vectorization.
- Direct Machine Code Generation (
v -native):
In addition to C emission, V contains built-in native backends for x86_64, ARM64, and WebAssembly (wasm32). These backends emit raw machine code directly without invoking an external C compiler.
+----------------+ +-------------+ +-------------------+ +--------------------+ +---------------+
| .v Source | ---> | Single-Pass | ---> | Static Safety & | ---> | C99 Emission or | ---> | Native Binary |
| Files | | Parser/AST | | Type Verification | | Direct x64/ARM/Wasm| | (< 1MB size) |
+----------------+ +-------------+ +-------------------+ +--------------------+ +---------------+Essential Compiler Flags Reference
| Flag | Purpose | Description |
|---|---|---|
v run <file.v> | Rapid Development | Compiles in memory with TCC and executes immediately. |
v -prod <file.v> | Production Release | Emits optimized C, applies -O3 / -flto, and strips debug symbols for minimal binary size. |
v -g <file.v> | Debugging | Generates DWARF debug symbols for use with GDB / LLDB / VSCode debugger. |
v -show-c-output <file.v> | Inspection | Prints or saves the generated C code so you can inspect compiler output directly. |
v fmt -w <file.v> | Formatting | Formats the source code according to the official V style guide. |
v -os <target> | Cross-Compilation | Cross-compiles for windows, linux, macos, or wasm32-wasi. |
Compiler Inspection Tip: You can always inspect the exact C code V generates for any program by passing v -o output.c hello.v. This is an incredible learning tool for understanding how V's high-level constructs translate to efficient, low-level C instructions.
Code Comments
Single Line Comments
In plain words: A single-line comment starts with //. Everything after // on that line is a note for humans and is completely ignored by the computer.
Comments are non-executable lines of text in a program that explain what the code does. They are ignored by the compiler but are essential for human developers. This lesson on Single Line Comments demonstrates how to write and format comments in V.
module main
// greet function prints greetings to the console
pub fn greet() {
println('Hello, Welcome to the Jungle!')
}
fn main() {
greet()
}Multi Line Comments
In plain words: A multi-line comment starts with /* and ends with */. Use it when your note spans several linesโand in V, you can even nest one /* ... */ inside another!
In V, multi-line (or block) comments are enclosed between /* and */.
Nested Block Comments: Unlike languages like C, C++, Java, or JavaScript, V supports nested block comments. This is a powerful feature that allows you to easily comment out large blocks of code even if they already contain block comments, without triggering syntax errors.
Code Hints & v-analyzer: Block comments (/* ... */) do not appear in code hints or hover tooltips when using v-analyzer (or v doc). To ensure your comments show up in IDE code hints and documentation, you must use single-line comments (//) on each line directly above the function, struct, or declaration.
module main
/*
multiply is a function that accepts two integer arguments (x and y).
It performs multiplication and returns the integer product.
Note: Comments formatted like this (/* ... */) do not show in code hints
using v-analyzer or `v doc`. You must use `//` for each line to enable IDE hints.
/*
Note: In V, block comments can be nested.
This is a nested block comment. In standard C, nesting block comments
would cause a compile error, but V's compiler parses them correctly.
*/
This is the end of the outer block comment.
*/
fn multiply(x int, y int) int {
return x * y
}
fn main() {
println(multiply(4, 5))
}Programm Commented All Places
In plain words: You can place comments almost anywhere in a V fileโabove functions, at the end of a line of code, or even right in the middle of an expressionโto explain what is happening.
Comments are non-executable lines of text in a program that explain what the code does. They are ignored by the compiler but are essential for human developers. This lesson on Programm Commented All Places demonstrates how to write and format comments in V.
module main
// Space3D A struct indicating the 3 dimensional coordinate system
struct Space3D {
mut:
x int
// x is an integer field that represents coordinate
y int
// y is an integer field that represents coordinate
z int
// z is an integer field that represents coordinate
}
/*
get_point is a function that returns a struct of Type Space3D with points x,y,z passed as input arguments to it
x is an input argument accepts values of type of int
y is an input argument accepts values of type of int
z is an input argument accepts values of type of int
get_point function returns a Struct result of type Space3D with its coordinates set as value passed as input arguments x, y and z
*/
fn get_point(x int, y int, z int) Space3D {
return Space3D{
x: x
y: y
z: z
}
}
const origin = get_point(0, 0, 0)
// Defining origin as a constant
fn main() {
// origin := Space3D {x: 0, y: 0, z:0}
println(origin)
}Chapter 2 Variables and Constants
Plain-English Chapter Guide (Start Here if You Are New to Coding!)
- The Big Idea in Simple Words: Think of a variable as a labeled storage box where your program keeps a piece of information (like a user's name or a game score). In V, every box is locked by default (immutable) so you don't accidentally overwrite important data. If you want a box whose value can change later (like a score that goes up), you simply put the word
mut(mutable = changeable) in front of it. A constant (const) is a permanent label set at the top of your file for values that never change, likepi = 3.14or your app's name. - Words to Know:
- Variable (
name := 'Alice'): A named box that holds a value. Created for the first time using:=. - Immutable (Locked by default): Once you put a value in a normal variable, it cannot be changed.
- Mutable (
mut score := 0): A variable marked withmutso you are allowed to change its value later using=(for example,score = 10). - Constant (
const): A fixed value declared outside of functions that stays the same forever. - Scope: The area (between
{and}) where a variable lives and can be used.
Quick Access โ Jump to any lesson in this chapter
Below is an index of all code examples in this chapter. You can use these links to jump directly to any specific code example:
Constants
- Define Single Constant
- Define Multiple Constants
- Define Constant Of Type Struct
- Define Constant Of Type Function
- Define Module Level Constants
- Cannot Define Constants Inside Functions
- Constants Module - Main (main.v)
- Constant Module Prefix - Helper (file1.v)
Variables
- Parallel Declaration Immutable Variables
- Parallel Declaration Mutable Variables
- Parallel Declaration Mut And Immutable Vars
- Augmented Assignment String
- Augmented Assignment Integer
- Declare Mutable Variable
- Cannot Update Mutable With Another Type
- Declare Immutable Variable
- Cannot Update Immutable Variables
- Declared And Assigned
- Declared And Not Assigned
- Unused Variables Will Be Warned
- Global Variables Not Allowed - Scope Demo
- Global Variables Not Allowed - File Scope Demo
- Variable Redeclaration
- Variable Scope For Same Variable Names
- Variable Shadowing Not Allowed
Variables are the basic storage units of any program. In this chapter, we explore how V handles variables with a safety-first mindset: variables are immutable by default, variable shadowing is forbidden, and constants are declared in module scopes. You will learn to manage program data safely and cleanly.
Under the Hood: Immutability, Memory Layout & Constant Inlining
๐ง Optional Deep Dive โ Click to Expand / Collapse
Plain-English Summary: In simple words: Keeping variables locked (immutable) by default lets V store them cleanly in fast computer memory and prevents accidental bugs where one part of your code changes a value another part was relying on.
V approaches variables and constants with a strict safety-first design philosophy. Understanding how memory is laid out and how the compiler optimizes bindings eliminates common debugging traps.
Memory Representation of Immutable vs Mutable Bindings
* Immutable Variables (x := 10):
When a variable is declared without mut, the V compiler registers it as an immutable binding. In the emitted C code, this translates to a const local variable allocated on the CPU stack. The compiler statically rejects any assignment or modification to this memory address.
* Mutable Variables (mut y := 10):
Declaring a variable with mut creates a standard mutable stack slot. Modifying y = 20 alters the value stored at that stack offset in-place without heap allocation.
* Type Rigidity:
Once a variable is declared, its type is permanently fixed. V prohibits changing the type of a mutable variable (e.g., assigning a string to an integer variable), preventing dynamic type confusion bugs.
Stack Frame:
+-------------------------------+-----------------------------------+
| const int x = 10 (Immutable) | int y = 20 (Mutable Stack Slot) |
+-------------------------------+-----------------------------------+Zero-Cost Compile-Time Constants
Constants declared in const (...) blocks possess unique runtime characteristics:
- Compile-Time Folding: Scalar constants (integers, floats, string literals) are evaluated and folded during compilation. The compiler substitutes the literal value directly at the callsite.
- Global Read-Only Data (
.rodata): Complex constants (structs, arrays) are emitted as static read-only symbols placed in the.rodatabinary section, incurring zero initialization runtime cost when your application boots. - Module Scoping: Constants are module-level and cannot be declared inside function bodies. This enforces clean separation between constant domain configuration and local procedural state.
Why Variable Shadowing is Strictly Prohibited
In many languages (such as JavaScript, Python, C++, and Rust), an inner block scope can redeclare a variable with the same name as an outer scope:
// FORBIDDEN IN V:
fn example() {
count := 10
if true {
count := 20 // Compile Error: duplicate variable name `count`
}
}V treats variable shadowing as a compile-time error. Shadowing is one of the most frequent sources of subtle logic bugs in large codebases (where an engineer intends to update an outer variable but accidentally declares a new local variable). By disallowing shadowing, V guarantees that every identifier within a function refers unambiguously to a single declared memory location.
Constants
Define Single Constant
In plain words: A constant (const) is a permanent, unchanging value given a name at the top of your file (like const app_name = 'V on Wheels').
Constants in V are defined using the const block. Constants are values that are known at compile time and never change throughout the execution of the program. By convention, constant names are written in lowercase, unlike many other languages.
This example shows how to define and use a single constant.
const app_name = 'V on Wheels'
fn main() {
println(app_name)
}Define Multiple Constants
In plain words: When you have several permanent values (like app settings, colors, or limits), you can group them all cleanly inside one const ( ... ) block.
You can define multiple constants within a single const block. This keeps related constants grouped together and makes the code cleaner.
This example shows how to declare multiple constants (integers, strings, floats) together.
const app_name = 'V on Wheels'
const max_connections = 1000
const decimal_places = 2
const pi = 3.14
fn main() {
println(app_name)
println(max_connections)
println(decimal_places)
println(pi)
}Define Constant Of Type Struct
In plain words: Constants aren't limited to simple numbers or text! You can also store a complete custom struct inside a const block so your whole program can share a fixed configuration object that nobody can accidentally change.
What happens in this example:
- Defines custom data blueprint(s):
Space3D. - Prints the result to the terminal window so you can see the output right away.
module main
struct Space3D {
mut:
x int
y int
z int
}
const origin = Space3D{
x: 0
y: 0
z: 0
}
fn main() {
println(origin)
}Define Constant Of Type Function
In plain words: You can even set a constant equal to the result returned by a helper function, or store a function reference as a constant so it stays fixed throughout your program.
What happens in this example:
- Defines custom data blueprint(s):
Space3D. - Creates helper function(s)/method(s):
get_point. - Prints the result to the terminal window so you can see the output right away.
module main
struct Space3D {
mut:
x int
y int
z int
}
fn get_point(x int, y int, z int) Space3D {
return Space3D{
x: x
y: y
z: z
}
}
const origin = get_point(0, 0, 0)
fn main() {
println(origin)
}Define Module Level Constants
In plain words: In V, constants (const) always live at the top level of a file (outside of fn main() or other functions) so every function in that module can read them cleanly.
What happens in this example:
- Prints the result to the terminal window so you can see the output right away.
module main
const app_name = 'V on Wheels'
fn main() {
println(app_name)
}Cannot Define Constants Inside Functions
In plain words: V does not allow const inside a function body. Why? Because inside a function, normal variables (x := 10) are already locked (immutable) by default! Use const only at the top of the file.
What happens in this example:
- Prints the result to the terminal window so you can see the output right away.
module main
const app_name = 'V on Wheels'
fn main() {
const greet = 'hi' // this is not top level constant definition, throws error.
println(app_name)
}Constants Module - Main (main.v)
In plain words: When your main.v file uses a public constant from another module (like mod1), you write the module's name in front of it (mod1.const_name) so it is always obvious which folder that constant came from.
module main
import mod1
fn main() {
mod1.do_work()
}Constant Module Prefix - Helper (file1.v)
In plain words: To let other folders/modules read a constant defined in this helper file, put pub const in front of it. Without pub, the constant stays private to its own folder.
What happens in this example:
- Creates helper function(s)/method(s):
do_work. - Prints the result to the terminal window so you can see the output right away.
module mod1
const greet_count = 5
pub fn do_work() {
println(greet_count)
}Variables
Parallel Declaration Immutable Variables
In plain words: You can create and fill two or more locked (immutable) variables on a single line separated by commas, like a, b := 10, 20.
In V, you can declare and initialize multiple variables in a single line. This is known as parallel declaration. By default, variables in V are immutable (read-only). Once assigned a value, they cannot be changed.
This program demonstrates declaring two variables a and b at the same time and assigning them initial values. Any attempt to modify a or b later in the code will cause a compile-time error.
fn main() {
first_name, last_name, age := 'Ada', 'Lovelace', 36
println('${first_name} ${last_name} is ${age} years old')
println('Next milestone: ${first_name} will speak at the conference')
}Parallel Declaration Mutable Variables
In plain words: This lesson (Parallel Declaration Mutable Variables) shows a clear, runnable V example from variables_and_constants/01_variables/01_variable_assignment/01_parallel_declaration/02_parallel\_\_declaration_mutable_variables.v that you can test directly in your terminal or the V Playground.
If you want to modify parallelly declared variables later, you must explicitly mark them as mutable using the mut keyword. In V, mutability is always explicit to make code safer and easier to reason about.
Here, we declare two mutable variables a and b at the same time using mut. We then reassign their values using the standard assignment operator (=).
fn main() {
mut greeting, mut recipient := 'Hi', 'world'
println('${greeting}, ${recipient}!')
greeting, recipient = 'Hello', 'Ada'
println('${greeting}, ${recipient}!')
}Parallel Declaration Mut And Immutable Vars
In plain words: V lets you create several variables on a single line separated by commas. Even better, you can mix changeable (mut) and locked (immutable) variables on the very same line!
What happens in this example:
- Uses
mutso the variable or collection is allowed to be updated after creation. - Prints the result to the terminal window so you can see the output right away.
fn main() {
mut message, count := 'Hello', 32
println(message)
message = 'Hi'
println(message)
println(count)
}Augmented Assignment String
In plain words: Instead of writing text = text + ' more' to tack extra words onto the end of a changeable (mut) string, you can use the shortcut += (add onto the existing value).
What happens in this example:
- Uses
mutso the variable or collection is allowed to be updated after creation. - Prints the result to the terminal window so you can see the output right away.
fn main() {
mut greeting := 'Hi'
println(greeting)
greeting = greeting + ' there'
println(greeting)
greeting += ', how are you today?'
println(greeting)
}Augmented Assignment Integer
In plain words: When updating a changeable (mut) number, shortcuts like += (add), -= (subtract), *= (multiply), and /= (divide) update the number in place without repeating the variable name twice.
What happens in this example:
- Uses
mutso the variable or collection is allowed to be updated after creation. - Prints the result to the terminal window so you can see the output right away.
fn main() {
mut score := 10
println(score)
score = score + 5
println(score)
score += 5
println(score)
}Declare Mutable Variable
In plain words: Putting mut in front of a new variable (mut age := 20) unlocks it so you can update its value later with = (age = 21).
By default, all variables in V are immutable (their values cannot change). To declare a variable whose value can be modified later, you must prepend the mut keyword before the variable name.
This example shows how to declare a mutable variable, change its value, and print the results.
fn main() {
mut counter := 0
counter += 1
println(counter)
}Cannot Update Mutable With Another Type
In plain words: Even when a variable is marked changeable (mut), its type is locked forever. If a box starts out holding a whole number (int), V will stop you from accidentally putting text (string) into that same box later.
What happens in this example:
- Uses
mutso the variable or collection is allowed to be updated after creation.
fn main() {
mut i := 10
i = 100
i = 'Apple' // throws error
}Declare Immutable Variable
In plain words: When you create a variable without mut (age := 20), V locks it automatically so no line of code can accidentally overwrite it later.
In V, variables are immutable by default. This design choice prevents accidental state mutation bugs, making code easier to reason about and safer for concurrency. When you declare a variable using the declaration operator :=, you are creating a read-only variable. If you try to reassign this variable later, the compilation will fail. This approach is similar to declaring constants in other languages, but it operates at the local scope level.
This example demonstrates how to declare an immutable variable and print its value.
fn main() {
// 'msg' is initialized as an immutable string variable using :=
msg := 'Hello'
println(msg)
}Cannot Update Immutable Variables
In plain words: If you try to change a locked variable that wasn't created with mut, the V compiler stops immediately and shows a helpful error message telling you how to fix it.
One of V's core safety features is immutability by default. If you declare a variable without the mut keyword and then try to reassign it a new value, the compiler will refuse to compile the program.
This example demonstrates what happens when you try to update an immutable variable (expect a compiler error).
fn main() {
msg := 'Hello'
msg = 'Good Day!' // throws error
}Declared And Assigned
In plain words: In V, creating a variable and giving it a starting value happen in one single step using :=. There are no 'uninitialized' mystery variables in V!
What happens in this example:
- Code note: declared and assigned
fn main() {
mut i := 0
// declared and assigned
println(i)
}Declared And Not Assigned
In plain words: In V, you are never allowed to create an empty variable box without putting a starting value in it. Every variable must be created and given a value at the same time using :=.
V does not allow variables to be declared without an initial value. Unlike other languages that initialize variables to a default 'zero' value or null, V forces you to explicitly provide a value. This prevents uninitialized variable bugs.
This example illustrates that declaring a variable without an assignment is a compilation error.
fn main() {
mut a // throws error
}Unused Variables Will Be Warned
In plain words: If you create a variable and never use it anywhere, V warns you so your code doesn't get cluttered with forgotten, leftover variables.
To keep codebases clean and efficient, the V compiler detects if you declare a variable but never use (consume) it. By default, V treats unused variables as a compilation warning/error, encouraging you to clean up dead code.
This example shows a declared variable that is never used.
fn main() {
i := 'hello' // i is not used anywhere, so warns when run in dev mode and throws error when run in prod mode
x := 3
y := 2
println(x + y)
}Global Variables Not Allowed - Scope Demo
In plain words: Variables created inside fn main() only live inside main(). Other functions cannot secretly reach in and grab them unless you pass them in as function inputs.
V does not allow global variables by default. Global state is a major source of bugs, race conditions in multi-threaded applications, and poor code structure. By forbidding globals, V enforces clean, modular code passing state via arguments.
These examples demonstrate that declaring variables outside of the main function or modules is strictly prohibited.
module main
fn method1() {
msg := 'Hello from Method1'
println(msg)
}
fn main() {
method1()
println(msg) // Will throw error as msg declared and accessible only in method1
}Global Variables Not Allowed - File Scope Demo
In plain words: V does not allow loose variables floating at the top of a file outside of functions (use const for fixed values instead). This prevents 'spaghetti code' where any file can unexpectedly change shared data.
V does not allow global variables by default. Global state is a major source of bugs, race conditions in multi-threaded applications, and poor code structure. By forbidding globals, V enforces clean, modular code passing state via arguments.
These examples demonstrate that declaring variables outside of the main function or modules is strictly prohibited.
module main
fn method1() {
if true {
mut b := 10
b++
}
println(b)
}
fn main() {
method1()
}Variable Redeclaration
In plain words: Once you create a variable with := (like a := 10), you cannot create a second variable with the exact same name in the same block. This prevents accidental copy-paste mistakes.
What happens in this example:
- Prints the result to the terminal window so you can see the output right away.
module main
fn main() {
x := 3
y := 2
println(x + y)
x := 5 // re-definition of variable x is not allowed
}Variable Scope For Same Variable Names
In plain words: Two different functions are completely separate roomsโso two different functions are allowed to each have their own local variable with the same name (like x) without colliding.
In V, variables are strictly scoped to the function or block in which they are declared. This lexical scoping means that two different functions can declare variables with the exact same name (e.g., msg) without any collision or interference. The compiler guarantees that these variables occupy separate locations in memory and are completely isolated from one another. This allows developers to use common, context-appropriate names like temp, id, or msg locally inside individual functions without worrying about global or cross-functional namespace pollution.
This program illustrates how msg is declared separately in both method1 and method2, showing scope isolation in action.
module main
fn method1() {
// 'msg' is local only to method1
msg := 'Hello from Method1'
println(msg)
}
fn method2() {
// 'msg' is local only to method2; does not conflict with method1's 'msg'
msg := 'Hello from Method2'
println(msg)
}
fn main() {
method1()
method2()
}Variable Shadowing Not Allowed
In plain words: Inside the same function, an inner { ... } block is not allowed to create a brand-new variable with the same name as one outside the block (shadowing), preventing confusing mix-ups.
Variable shadowing happens when a variable declared within an inner scope (like an if block, a loop, or a function body) has the same name as a variable in an outer scope. V strictly forbids variable shadowing at the compiler level. Prohibiting shadowing prevents a class of common bugs where a developer accidentally updates a local inner variable instead of the intended outer variable, or vice versa.
This example demonstrates how V rejects shadowed variable declarations.
module main
fn scope_demo() {
// 'x' is declared in the function's main scope
x := 10
println(x)
if true {
// ERROR: Declaring another variable named 'x' in an inner block is forbidden.
// To fix this, you must name the inner variable something else.
x := 20
println(x)
}
println(x)
}
fn main() {
scope_demo()
}Chapter 3 Primitive Data Types
Plain-English Chapter Guide (Start Here if You Are New to Coding!)
- The Big Idea in Simple Words: Just like in everyday life we use different kinds of informationโwhole numbers for counting apples, decimals for money, words for names, and yes/no checkboxesโV has basic data types (primitive types) for each kind of value. Knowing which type you have helps V keep your math accurate and your text clean.
- Words to Know:
- Boolean (
bool): A simple Yes or No switchโeithertrueorfalse. - Integer (
int): A whole number without decimals, like42,0, or-7. (i8,i16,i64are just smaller or bigger box sizes;u8,u32,u64are unsigned, meaning 0 or positive only). - Float (
f64,f32): A number with a decimal point, like3.14or99.95. - String (
string): Any piece of text inside single quotes, like'Hello world'. You can plug variables directly into text using${variable}(string interpolation). - Rune (
rune): A single letter, symbol, or emoji wrapped in backticks, likeAor๐``.
Quick Access โ Jump to any lesson in this chapter
Below is an index of all code examples in this chapter. You can use these links to jump directly to any specific code example:
Primitive Types Demo
Boolean Type
Numeric Types
- Declaring Integers
- Hex Binary Octa Notation Of Declaring Integers
- Promoting Numeric Types
- Arithmetic Operators
- Bitwise Operators
- Shift Operators
- Shift Operator On Range Of Integers
- Integer Methods
- Float Methods
- U8 Methods
- Size Pointer Methods
Rune Type
String Type
- Declare String
- String Read Only Array Of Bytes
- Strings Immutable By Default
- Declaring Mutable Strings
- Cannot Mutate String Elements
- String Interpolation
- Escape Special Characters
- Declare Raw Strings
- String Concatenation Using Plus Sign
- String Concatenation Using Interpolation
- Extract Substring From String Literal
- Split String
- String To Runes Array
- Count Sub String Occurences
- Check String Contains Substring
- String Contains Is Case Sensitive
- Common String Methods
Primitive Types Demo
Primitive Types Demo Code
In plain words: A complete tour of V's built-in basic types: Yes/No switches (bool), text (string), single characters (rune), whole numbers (int, i8..i64, u8..u64), and decimals (f32, f64).
This comprehensive example demonstrates every primitive data type in V:
- Boolean:
bool(representingtrueorfalse). - String:
string(representing an immutable array of bytes). - Rune:
rune(representing a single Unicode code point, alias foru32). - Signed Integers:
i8(8-bit),i16(16-bit),int(32-bit),i64(64-bit). - Unsigned Integers:
u8(8-bit),u16(16-bit),u32(32-bit),u64(64-bit). - Platform-dependent sizes:
isize(signed size of a pointer),usize(unsigned size of a pointer). - Floating Point Numbers:
f32(32-bit single-precision),f64(64-bit double-precision).
For each type, the example initializes a value and prints its value, type (using typeof(var).name), and size in bytes (using sizeof(var)).
module main
fn main() {
println('==================================================')
println(' Vlang Primitive Data Types Demo ')
println('==================================================')
// 1. Boolean Type
b := true
println('Boolean: val: ${b} | type: ${typeof(b).name} | size: ${sizeof(b)} byte')
// 2. String Type
s := 'Hello, V!'
println('String: val: "${s}" | type: ${typeof(s).name} | size: ${sizeof(s)} bytes')
// 3. Rune Type (unicode character, represented as `r` prefix or backticks)
r := `V`
println('Rune: val: ${r} (char: ${r.str()}) | type: ${typeof(r).name} | size: ${sizeof(r)} bytes')
// 4. Signed Integers
i_8 := i8(-128)
i_16 := i16(-32768)
i_32 := int(-2147483648)
i_64 := i64(-9223372036854775808)
println('i8: val: ${i_8} | type: ${typeof(i_8).name} | size: ${sizeof(i_8)} byte')
println('i16: val: ${i_16} | type: ${typeof(i_16).name} | size: ${sizeof(i_16)} bytes')
println('int: val: ${i_32} | type: ${typeof(i_32).name} | size: ${sizeof(i_32)} bytes')
println('i64: val: ${i_64} | type: ${typeof(i_64).name} | size: ${sizeof(i_64)} bytes')
// 5. Unsigned Integers
u_8 := u8(255)
u_16 := u16(65535)
u_32 := u32(4294967295)
u_64 := u64(18446744073709551615)
println('u8: val: ${u_8} | type: ${typeof(u_8).name} | size: ${sizeof(u_8)} byte')
println('u16: val: ${u_16} | type: ${typeof(u_16).name} | size: ${sizeof(u_16)} bytes')
println('u32: val: ${u_32} | type: ${typeof(u_32).name} | size: ${sizeof(u_32)} bytes')
println('u64: val: ${u_64} | type: ${typeof(u_64).name} | size: ${sizeof(u_64)} bytes')
// 6. Platform-dependent Sizes
isize_val := isize(-12345)
usize_val := usize(12345)
println('isize: val: ${isize_val} | type: ${typeof(isize_val).name} | size: ${sizeof(isize_val)} bytes')
println('usize: val: ${usize_val} | type: ${typeof(usize_val).name} | size: ${sizeof(usize_val)} bytes')
// 7. Floating Point Numbers
f_32 := f32(3.14159)
f_64 := f64(2.718281828459)
println('f32: val: ${f_32} | type: ${typeof(f_32).name} | size: ${sizeof(f_32)} bytes')
println('f64: val: ${f_64} | type: ${typeof(f_64).name} | size: ${sizeof(f_64)} bytes')
println('==================================================')
}V is a statically-typed language, meaning every variable has a fixed data type at compile time. In this chapter, you will learn about V's primitive types: booleans for logic, numeric types for numbers, runes for single characters, and strings for text. You will also learn about V's rich set of built-in methods on these types.
Under the Hood: Primitive Data Representation, Strings & UTF-8
๐ง Optional Deep Dive โ Click to Expand / Collapse
Plain-English Summary: In simple words: Every number or piece of text takes up a small, predictable number of bytes in memory. V strings store their length up front so checking how long a string is happens instantly!
Every primitive type in V is engineered for predictable memory footprint, binary compatibility with C, and strict safety.
Primitive Memory Map & Sizing
| V Type | C Equivalent | Size (Bytes) | Range / Purpose |
|---|---|---|---|
bool | bool | 1 byte | true or false |
i8 / u8 | int8_t / uint8_t | 1 byte | -128 to 127 / 0 to 255 (byte) |
i16 / u16 | int16_t / uint16_t | 2 bytes | 16-bit signed / unsigned integers |
int / i32 / u32 | int32_t / uint32_t | 4 bytes | Standard 32-bit integer |
i64 / u64 | int64_t / uint64_t | 8 bytes | 64-bit signed / unsigned integers |
f32 / f64 | float / double | 4 / 8 bytes | IEEE 754 floating-point numbers |
rune | uint32_t | 4 bytes | Unicode code point (u32) |
isize / usize | ptrdiff_t / size_t | 4 or 8 bytes | Pointer-sized integers |
The Memory Layout of a V `string`
In V, a string is a 16-byte structure on 64-bit systems. It is defined internally as:
struct string {
pub:
str &u8 // Pointer to raw UTF-8 byte array (null-terminated)
len int // Length of the string in bytes
is_lit int // 1 if string literal (static .rodata), 0 if heap-allocated
}Key architectural benefits of this design:
- Zero-Copy C Interoperability: Because the underlying
strbyte buffer is guaranteed to be null-terminated (\0), passing a V string to any C library function (printf,sqlite3_open,fopen) requires onlys.strโwith zero memory allocation or copying. - Immutability & Safety: V strings are immutable. Slicing a string (
s[0..5]) creates a new string header pointing to the same memory segment without deep copying.
Byte Indexing (`s[i]`) vs Unicode Runes (`s.runes()`)
Because V strings are UTF-8 encoded:
s[i]accesses the raw byte (u8) at byte offsetiin $O(1)$ constant time.- ASCII characters occupy 1 byte, while international characters (e.g.
รฑ,ๆฅ,๐ฆ) occupy between 2 to 4 bytes. - To iterate over human-readable characters, call
s.runes(), which decodes UTF-8 bytes into 4-byterune(u32) Unicode code points.
String Interpolation Internals
When you write:
greeting := 'Hello, ${name}! You have ${unread_count} new messages.'The V compiler does not invoke a slow runtime formatting engine (like sprintf). Instead, it analyzes the template at compile time and emits optimized calls to strings.new_builder(estimated_size), appending each component directly into a pre-sized buffer.
Boolean Type
Logical Operators
In plain words: Logical operators let you combine Yes/No (true/false) questions: && (AND) is true only when both sides are true, || (OR) is true when at least one side is true, and ! (NOT) flips true to false (and vice versa).
What happens in this example:
- Code note: Logical And using && operator
- Code note: Logical OR using || operator
- Code note: Logical not using ! Operator
module main
fn main() {
t := true
f := false
// Logical And using && operator
and_tt := t && t
and_tf := t && f
and_ft := f && t
and_ff := f && f
println('Logical And using && operator')
println('${t} && ${t} = ${and_tt}')
println('${t} && ${f} = ${and_tf}')
println('${f} && ${t} = ${and_ft}')
println('${f} && ${f} = ${and_ff}')
println('')
// Logical OR using || operator
or_tt := t || t
or_tf := t || f
or_ft := f || t
or_ff := f || f
println('Logical OR using || Operator')
println('${t} || ${t} = ${or_tt}')
println('${t} || ${f} = ${or_tf}')
println('${f} || ${t} = ${or_ft}')
println('${f} || ${f} = ${or_ff}')
println('')
// Logical not using ! Operator
not_t := !t
not_f := !f
println('Logical not using ! Operator')
println('!${t} = ${not_t}')
println('!${f} = ${not_f}')
}Relational Operators
In plain words: Relational (comparison) operators compare two values and answer true or false: == (equals), != (does not equal), < (less than), > (greater than), <= (less than or equal to), and >= (greater than or equal to).
What happens in this example:
- Code note: Comparing using Relational operator >
- Code note: Comparing using Relational operator ==
- Code note: Comparing using Relational operator !=
module main
struct Note {
id int
detail string
completed bool
}
fn main() {
mut n := Note{
id: 1001
detail: 'get groceries'
}
println(n.completed) // un-assigned bool field will be false by default
// Comparing using Relational operator >
if n.id > 1000 { // comparison of note id of integer type to another integer evaluates to a boolean
println('The note id is greater than 1000')
} else {
println('The note id is less than 1000')
}
// Comparing using Relational operator ==
if n.detail == 'get groceries' {
println('The note details about groceries')
}
// Comparing using Relational operator !=
if n.detail != 'get dairy products' {
println('The note does not details about dairy products')
}
}Boolean Methods
In plain words: Even a simple true or false (bool) value in V has a .str() method to turn it into the text word 'true' or 'false'.
Booleans in V are simple true or false values. V provides built-in methods on boolean types, such as str(), which converts the boolean value to its string representation ('true' or 'false').
This is useful for logging, printing, or interpolating booleans into strings.
module main
fn main() {
t := true
f := false
// str() returns string representation ('true' or 'false')
println(t.str()) // true
println(f.str()) // false
}Numeric Types
Declaring Integers
In plain words: Whole numbers default to int (32-bit), which handles numbers up to about 2 billion. You can also pick smaller or larger whole-number sizes (i8, i16, i64, u8, u16, u32, u64) when needed.
V has several built-in integer types, both signed and unsigned, of various sizes (e.g., i8, i16, i32, i64 for signed integers, and u8, u16, u32, u64 for unsigned integers). If you declare an integer using :=, V defaults to the standard 32-bit integer (int).
This example demonstrates how to declare different integer types.
module main
fn main() {
x := 1
println(typeof(x).name)
// int
i := 1_000
j := 1000
println(i == j) // true
}Hex Binary Octa Notation Of Declaring Integers
In plain words: Whole numbers default to int (32-bit), which handles numbers up to about 2 billion. You can also pick smaller or larger whole-number sizes (i8, i16, i64, u8, u16, u32, u64) when needed.
V has several built-in integer types, both signed and unsigned, of various sizes (e.g., i8, i16, i32, i64 for signed integers, and u8, u16, u32, u64 for unsigned integers). If you declare an integer using :=, V defaults to the standard 32-bit integer (int).
This example demonstrates how to declare different integer types.
module main
fn demo() {
h1 := 0x64 // hexadecimal starts with 0x
b1 := 0b1100100 // binary starts with 0b
o1 := 0o144 // Octal starts with 0o
println('Value of var h1 with hexadecimal value : ${h1}')
println('Data type of var h1 with hexadecimal value : ${typeof(h1).name}')
println('Value of var b1 with binary value : ${b1}')
println('Data type of var b1 with binary value : ${typeof(b1).name}')
println('Value of var o1 with octal value : ${o1}')
println('Data type of var o1 with octal value : ${typeof(o1).name}')
}
fn main() {
demo()
}Promoting Numeric Types
In plain words: To prevent accidental rounding mistakes, V never mixes different number sizes behind your back. To add an i16 and an int, you explicitly convert one to match the other (like int(my_small_num)).
V is very strict about types. It does not perform implicit type conversion (coercion) between different numeric types to prevent accidental precision loss or overflow bugs. If you want to perform arithmetic operations on different types, you must explicitly cast them.
This example shows how to cast (promote) smaller integer types to larger ones or to floats.
module main
fn demo() {
ia := i8(2)
ib := i16(2)
ic := int(2)
println('----type definitions----')
println('variable ia is of type: ${typeof(ia).name}')
println('variable ib is of type: ${typeof(ib).name}')
println('variable ic is of type: ${typeof(ic).name}')
println('')
iaa := ia + ia // i8 with i8 results i8
ibb := ib + ib // i16 with i16 results i16
icc := ic + ic // int with int results int
println('----mixing types----')
println('variable iaa is of type: ${typeof(iaa).name}, after adding type ${typeof(ia).name} with itself')
println('variable ibb is of type: ${typeof(ibb).name}, after adding type ${typeof(ib).name} with itself')
println('variable icc is of type: ${typeof(icc).name}, after adding type ${typeof(ic).name} with itself')
println('')
iab := ia + ib // i8 with i16 results in i16
ibc := ib - ic // i16 with i32 results in i32
println('----type promotion----')
println('variable iab is promoted to type: ${typeof(iab).name}, after adding type ${typeof(ia).name} with ${typeof(ib).name}')
println('variable ibc is promoted to type: ${typeof(ibc).name}, after subtracting type ${typeof(ib).name} with ${typeof(ic).name}')
iba := ib / ia // the division of i16 and i8 types
println('Variable iba is promoted to the higher data type ${typeof(iba).name} which is carried from ib of type ${typeof(ib).name} divided from variable ia of type ${typeof(ia).name}')
fa := f32(2)
fa_iba := fa + iba // fa is type of f32 and iba is of type i32
println('Variable fa_iba is promoted to the higher data type ${typeof(fa_iba).name} which is carried from fa of type ${typeof(fa).name} when added with variable iba of type ${typeof(iba).name}')
}
fn main() {
demo()
}Arithmetic Operators
In plain words: Arithmetic operators do everyday math on numbers: + (add), - (subtract), * (multiply), / (divide), and % (remainder after division, like 10 % 3 == 1).
What happens in this example:
- Code note: add using +
- Code note: subtract using -
- Code note: product using *
- Code note: / results in quotient
module main
fn main() {
a := 10
b := 2
// add using +
sum := a + b
// subtract using -
diff := b - a
// product using *
prod := a * b
// / results in quotient
quotient := a / b
// % modulo results in remainder
remainder := a % b
println('Sum of ${a} and ${b} is ${sum}')
println('Subtracting ${a} from ${b} is ${diff}')
println('Product of ${a} and ${b} is ${prod}')
println('Quotient when ${a} divided by ${b} is ${quotient}')
println('Remainder when ${a} divided by ${b} is ${remainder}')
}Bitwise Operators
In plain words: Computers store numbers as rows of 1s and 0s (bits). Bitwise operators (& AND, | OR, ^ XOR, ~ NOT) compare or flip those individual 1s and 0s directlyโhandy for hardware flags, graphics, and permissions.
What happens in this example:
- Code note: bitwise AND operation of two binary nums using & operator
- Code note: bitwise OR operation of two binary nums using | operator
- Code note: bitwise XOR operation of two binary nums using ^ operator
- Code note: bitwise NOT operation of an binary nums using ~ operator
module main
fn main() {
a := 0b00000110 // 6
b := 0b00000010 // 2
// bitwise AND operation of two binary nums using & operator
b_and := a & b
// bitwise OR operation of two binary nums using | operator
b_or := a | b
// bitwise XOR operation of two binary nums using ^ operator
b_xor := a ^ b
// bitwise NOT operation of an binary nums using ~ operator
not_a := ~a // Not operation yields value which is equal to -(a+1) in its integer form
println('Bitwise AND: ${a:08b} & ${b:08b} = ${b_and:08b}')
println('Bitwise OR: ${a:08b} | ${b:08b} = ${b_or:08b}')
println('Bitwise XOR: ${a:08b} ^ ${b:08b} = ${b_xor:08b}')
println('Bitwise NOT: ~${a:b} = ${not_a:b}')
}Shift Operators
In plain words: Bit shift operators slide a number's binary 1s and 0s left (<<, which doubles the number each step) or right (>>, which halves the number each step).
What happens in this example:
- Code note: declare 8 bit integer with value 3
- Code note: 8 bits equals to 1 byte
- Code note: declare 8-bit unsigned integer to shift by 1 position
- Code note: Shift left the value 3 by 1 position
module main
fn main() {
// declare 8 bit integer with value 3
a := i8(3)
// 8 bits equals to 1 byte
println('a is ${sizeof(a)} byte(s)') // a is 1 byte(s)
// declare 8-bit unsigned integer to shift by 1 position
pos := u8(1)
// Shift left the value 3 by 1 position
a_left_shift := a << pos
println('${a} << ${pos} = ${a_left_shift}')
}Shift Operator On Range Of Integers
In plain words: Shifting bits left (<<) multiplies a number by powers of 2 ($2^1, 2^2, 2^3...$). Looping through a range and shifting 1 << i is an easy way to generate powers of two (1, 2, 4, 8, 16...).
What happens in this example:
- Prints the result to the terminal window so you can see the output right away.
module main
fn main() {
val := i8(1)
bits := sizeof(val) * 8
println('Performing left shift using << Operator')
for i in 0 .. bits {
after_shift := val << i
println('${val} << ${i} = ${after_shift} // type after shift operation: ${typeof(after_shift).name}')
}
}Integer Methods
In plain words: Whole numbers (int) in V come with built-in helper methods like .str() (turn the number into text) and .hex() (show the number in base-16 hexadecimal format).
What happens in this example:
- Code note: str() returns string representation of the integer
- Code note: hex() returns hexadecimal representation without prefix
- Code note: hex2() returns hexadecimal representation with "0x" prefix
- Code note: hex_full() returns hexadecimal representation with full width padding for the type (8 digits for 32-bit int)
module main
fn main() {
x := 42
// str() returns string representation of the integer
println(x.str()) // "42"
// hex() returns hexadecimal representation without prefix
println(x.hex()) // "2a"
// hex2() returns hexadecimal representation with "0x" prefix
println(x.hex2()) // "0x2a"
// hex_full() returns hexadecimal representation with full width padding for the type (8 digits for 32-bit int)
println(x.hex_full()) // "0000002a"
}Float Methods
In plain words: Decimal numbers (f32, f64) have built-in helpers to convert them into formatted text strings or inspect their value cleanly.
What happens in this example:
- Code note: str() returns string representation of the float
- Code note: strg() returns string representation (often identical to str())
- Code note: strlong() returns a full/long string representation of the float
- Code note: strsci(precision) returns scientific notation with specified precision/decimal places
module main
fn main() {
f := 12345.6789
// str() returns string representation of the float
println(f.str()) // "12345.6789"
// strg() returns string representation (often identical to str())
println(f.strg()) // "12345.6789"
// strlong() returns a full/long string representation of the float
println(f.strlong()) // "12345.6789"
// strsci(precision) returns scientific notation with specified precision/decimal places
println(f.strsci(4)) // "1.2346e+04"
// eq_epsilon(other) performs a comparison using machine epsilon (for near-equality)
f2 := 12345.678900000001
println(f.eq_epsilon(f2)) // true
}U8 Methods
In plain words: A u8 (a single byte, from 0 to 255) often represents a single ASCII text character. V gives u8 handy methods like .ascii_str(), .is_digit(), .is_letter(), and .is_space().
What happens in this example:
- Code note: str() returns string representation of the numeric value
- Code note: ascii_str() returns string of length 1 containing the character
- Code note: hex() returns hexadecimal representation
- Code note: hex_full() returns hexadecimal representation (same as hex() for u8)
module main
fn main() {
b := u8(65) // ASCII code for 'A'
// str() returns string representation of the numeric value
println(b.str()) // "65"
// ascii_str() returns string of length 1 containing the character
println(b.ascii_str()) // "A"
// hex() returns hexadecimal representation
println(b.hex()) // "41"
// hex_full() returns hexadecimal representation (same as hex() for u8)
println(b.hex_full()) // "41"
// is_alnum() checks if the character is alphanumeric
println(b.is_alnum()) // true
// is_bin_digit() checks if the character is a binary digit ('0' or '1')
println(b.is_bin_digit()) // false
// is_capital() checks if the character is an uppercase letter
println(b.is_capital()) // true
// is_digit() checks if the character is a decimal digit ('0'-'9')
println(b.is_digit()) // false
// is_hex_digit() checks if the character is a hexadecimal digit ('0'-'9', 'a'-'f', 'A'-'F')
println(b.is_hex_digit()) // true
// is_letter() checks if the character is an alphabetic letter
println(b.is_letter()) // true
// is_oct_digit() checks if the character is an octal digit ('0'-'7')
println(b.is_oct_digit()) // false
// is_space() checks if the character is a whitespace character
println(b.is_space()) // false
// repeat(count) repeats the character count times and returns a string
println(b.repeat(3)) // "AAA"
// str_escaped() returns an escaped string representation of the character
println(b.str_escaped()) // "A"
}Size Pointer Methods
In plain words: isize and usize are whole-number types that match your computer's memory-address size (64-bit on modern computers) and include helper methods for converting to text.
What happens in this example:
- Code note: isize and usize methods
- Code note: str() returns string representation
- Code note: voidptr methods
- Code note: str() returns the memory address as string
module main
fn main() {
// isize and usize methods
sz := isize(100)
usz := usize(200)
// str() returns string representation
println(sz.str()) // "100"
println(usz.str()) // "200"
// voidptr methods
x := 42
p := voidptr(&x)
// str() returns the memory address as string
println(p.str().starts_with('0x')) // true
// hex_full() returns full-width hex representation of address
println(p.hex_full().len > 0) // true
// vbytes(len) returns a byte array representation of the memory pointed to (must be called in unsafe block)
unsafe {
bytes := p.vbytes(int(sizeof(int)))
println(bytes) // [42, 0, 0, 0]
}
}Rune Type
Declare Rune
In plain words: A rune holds one single character, symbol, or emoji wrapped in backticks (like A or ๐ holds text of any length wrapped in single quotes (), whereas a string'Hello').
A rune in V represents a single Unicode code point. Runes are declared using backticks (e.g., \a\, \๐ฅ\) and are represented internally as 32-bit unsigned integers (u32). This allows V to support multi-byte Unicode characters (like emojis or Chinese characters) as single character tokens.
This example shows how to declare and print runes.
fn main() {
// A rune stores a single Unicode character.
l := `a`
println(typeof(l).name)
// rune
}Rune Operations With Strings
In plain words: You can convert back and forth between a single character (rune) and text (string), or turn a string into a list of runes with .runes().
Since strings in V are arrays of UTF-8 encoded bytes, you cannot directly check for a rune inside a string using string operations unless the rune is first converted to a string. V provides .str() on the rune type to easily convert a rune to a 1-character string, allowing you to use string methods like .count(), .contains(), etc.
This example shows how to count occurrences of a Unicode rune in a string.
fn main() {
beverage := 'cafรฉ'
// 's' is a rune representing the Unicode character 'รฉ'
s := `รฉ`
// Since .count() expects a string argument, we convert the rune 's' using .str()
println(beverage.count(s.str()))
// Outputs: 1
}Rune Methods
In plain words: Single characters (rune) have built-in methods like .to_upper(), .to_lower(), .str(), and .length_in_bytes().
Runes in V are not just raw numbers; they are full-fledged Unicode characters that support several built-in methods. You can convert their case, check their byte length (which can range from 1 to 4 bytes depending on the character, such as emojis), obtain their byte array representation, or convert them to hexadecimal code points.
This lesson demonstrates common helper methods on the rune type.
module main
fn main() {
r := `A`
// bytes() returns the byte representation (UTF-8 bytes) of the rune
println(r.bytes()) // [65]
// hex() returns the hexadecimal representation of the rune code point
println(r.hex()) // "41"
// length_in_bytes() returns the size of the rune in bytes (1 to 4)
println(r.length_in_bytes()) // 1
// repeat(count) returns a string with the rune repeated count times
println(r.repeat(3)) // "AAA"
// str() returns the string representation of the rune
println(r.str()) // "A"
// to_lower() returns the lowercase rune
println(r.to_lower().str()) // "a"
// to_upper() returns the uppercase rune
println(r.to_upper().str()) // "A"
// to_title() returns the titlecase rune
println(r.to_title().str()) // "A"
// Testing with a multi-byte UTF-8 rune (dog emoji ๐)
r2 := `๐`
println(r2.bytes()) // [240, 159, 144, 149]
println(r2.hex()) // "1f415" (Unicode code point in hex)
println(r2.length_in_bytes()) // 4
println(r2.repeat(2)) // "๐๐"
println(r2.str()) // "๐"
}String Type
Declare String
In plain words: Strings hold text inside single quotes ('hello') or double quotes ("hello"), and .len tells you how many bytes long the text is.
In V, strings are representing read-only arrays of bytes, encoded natively in UTF-8. You can declare string variables using single quotes ('hello') or double quotes ("hello"), though single quotes are preferred in idiomatic V. The string type in V comes with built-in metadata, such as the .len field, which returns the total number of bytes in the string (not necessarily the number of Unicode characters/runes). You can inspect the runtime type of any variable using V's built-in typeof() function.
This example illustrates how to declare string variables, concatenate them, check string lengths, and query variable types at runtime.
module main
fn main() {
// Strings can be declared using single quotes
greeting := 'hello'
name := 'Ada'
// String concatenation using the '+' operator
message := greeting + ', ' + name + '!'
println(message)
// Access the length (in bytes) of the string using the .len field
println('Length: ${message.len}')
// Inspect the variable type at runtime using typeof()
println('Type: ${typeof(message).name}')
}String Read Only Array Of Bytes
In plain words: Underneath, a string is a numbered row of byte values (u8). Indexing s[0] gives you the first byte, and s[0].ascii_str() shows it as a letter.
In V, strings are represented internally as read-only arrays of UTF-8 encoded bytes (u8). This means you can index into a string using bracket notation (str[index]) to extract the raw byte value at that position. The type returned from indexing a string is u8 (8-bit unsigned integer), not a string or rune.
This example shows how to read raw bytes from a string and prints the byte value and its type name.
fn main() {
fruit := 'Orange'
// Accessing the first byte of the string 'Orange'.
// This returns the ASCII value of 'O' (which is 79), and its type is 'u8'.
println(typeof(fruit[0]).name)
// Outputs: u8
println(fruit[0])
// Outputs: 79
}Strings Immutable By Default
In plain words: Just like other variables in V, a string variable cannot be pointed to new text unless you declare the variable with mut.
Strings in V are completely immutable. Once a string is created, its characters cannot be modified in place. Any operation that manipulates a string (such as replacing characters or converting to uppercase) returns a brand new string instead of modifying the original.
This example demonstrates that strings are read-only and cannot be changed.
fn main() {
s := 'hello'
// variable s is immutable
s = 'Hello!'
// this results in error
}Declaring Mutable Strings
In plain words: Adding mut to a string variable (mut s := 'hello') lets you replace its text or tack on more words with +=.
While strings are immutable, you can declare a mutable string variable using mut. This allows the variable to be reassigned to a new string value, or appended to using the += operator.
This example shows how to declare a mutable string and append text to it.
fn main() {
mut msg := 'Hello Friend!'
msg = 'Hope you are doing good.'
println(msg)
// Hope you are doing good.
msg = msg + ' There is a surprise for you.'
println(msg)
// Hope you are doing good. There is a surprise for you.
}Cannot Mutate String Elements
In plain words: Even on a mut string variable, you cannot overwrite a single letter by index (s[0] = 'J' is forbidden); instead, build a new string or use .replace(...).
Although declaring a string variable with mut lets you reassign the variable to reference a completely different string, it does NOT let you mutate individual characters or bytes within the existing string (e.g. s[0] = \G\``). String data elements are strictly read-only arrays of bytes, and attempting to mutate them will fail compilation.
This program shows that element mutation is strictly forbidden.
fn main() {
mut greet := 'good Day'
// ERROR: Cannot assign directly to string indices.
// Strings in V are read-only byte arrays and their contents cannot be mutated.
greet[0] = 'G'
}String Interpolation
In plain words: Put ${variable} or ${expression} right inside a string literal and V will automatically plug the value into the text for you!
String interpolation is a clean way to insert variables or expressions inside a string literal. In V, you do this by wrapping the variable or expression in ${variable} inside a single-quoted string.
This example demonstrates how to format strings with variable values.
fn main() {
a := 'coding'
b := 'fun'
println('${a} is ${b}')
println('${a} is ${b}')
}Escape Special Characters
In plain words: Backslash codes like \n (new line), \t (tab indent), and \' (single quote) let you include special formatting characters inside a string.
V strings support standard escape characters (like \n for newlines, \t for tabs, and \\ for backslashes) to represent special characters inside a string literal.
This example shows how these escape sequences are rendered in the console.
module main
fn main() {
// 1. Newline escape character (\n)
println('Hello\nWorld!')
// 2. Tab escape character (\t)
println('Name:\tAlice\tAge:\t25')
// 3. Backslash escape character (\\)
println('File path: C:\\Program Files\\V')
// 4. Escaping single quotes (\') in a single-quoted string
println("It's my Daughter's birthday!")
// 5. Escaping double quotes (\") in a double-quoted string
println('She said, "V is fast!"')
}Declare Raw Strings
In plain words: Putting an r in front of a string (r'C:\Users\name') creates a raw string where backslashes \ are treated as plain everyday characters instead of escape codes.
If you want to write a string literal where escape sequences (like \n) are treated as literal text instead of special commands, you can declare a raw string by prefixing the string literal with r (e.g., r\'hello\nworld\').
This is extremely useful when writing regular expressions or file paths.
module main
fn main() {
i := r'hi \how are you/?'
println(i)
}String Concatenation Using Plus Sign
In plain words: You can glue two or more strings together into one longer string using the + sign (like 'Hello, ' + 'World!').
In V, joining strings together is performed using the + operator. Since strings are immutable byte arrays, each concatenation creates a brand-new string in memory and copies the contents of both source strings. While the + operator is extremely convenient and clear for joining a few strings, doing this in loops or performance-critical code paths is discouraged because it leads to excessive memory allocations. For high-performance string building, V offers the strings.Builder module.
This example illustrates the direct concatenation of two string variables.
module main
fn main() {
a := 'con'
b := 'cat'
// Concatenate a and b, creating a new string 'concat' in memory
println(a + b)
// concat
}String Concatenation Using Interpolation
In plain words: Instead of gluing strings together with +, you can drop variables directly inside a string using ${variable}. This is called string interpolation, and it is the cleanest way to build messages in V.
What happens in this example:
- Prints the result to the terminal window so you can see the output right away.
module main
fn main() {
i := 1
j := 'man army'
println('${i} ${j}')
}Extract Substring From String Literal
In plain words: Use slice brackets s[start..end] or .substr(start, end) to cut out a smaller piece of text from inside a string.
V provides two main techniques to extract substrings from a string literal or variable:
- The
.substr(start, end)Method: Takes the starting index (inclusive) and ending index (exclusive) as parameters. - Range Slicing Syntax
[start..end]: A clean and idiomatic syntax (similar to Go and Rust) where you specify range offsets. If the starting index is omitted (e.g.[..end]), it defaults to0. If the ending index is omitted (e.g.[start..]), it defaults to the length of the string.
Both techniques are demonstrated in the example below.
module main
fn main() {
a := 'Camel'
// Method 1: Using the substr(start, end) method
// Extracts characters from index 0 up to (but not including) index 3
b := a.substr(0, 3)
println(b) // Output: Cam
// Method 2: Using idiomatic range slicing syntax [start..end] (similar to Go/Rust)
// Slices from index 1 up to (but not including) index 4
c := a[1..4]
println(c) // Output: ame
// Method 3: Slicing from start to index [..end]
// If the start index is omitted, it defaults to 0
d := a[..3]
println(d) // Output: Cam
// Method 4: Slicing from index to end [start..]
// If the end index is omitted, it defaults to the string length
e := a[2..]
println(e) // Output: mel
}Split String
In plain words: The .split(...) method chops a piece of text into a list (array) of smaller words or pieces wherever a separator character (like a space ' ' or comma ',') appears.
What happens in this example:
- Code note: split by space as delimiter
- Code note: ['The', 'tiny', 'tiger', 'tied', 'the', 'tie', 'tighter', 'to', 'its', 'tail']
module main
fn main() {
sp := 'The tiny tiger tied the tie tighter to its tail'
res := sp.split(' ')
// split by space as delimiter
println(typeof(res).name)
// []string
println(res)
// ['The', 'tiny', 'tiger', 'tied', 'the', 'tie', 'tighter', 'to', 'its', 'tail']
}String To Runes Array
In plain words: Because special characters and emojis can take more than one byte in memory, calling .runes() turns a text string into an accurate list of individual visual characters ([]rune).
What happens in this example:
- Prints the result to the terminal window so you can see the output right away.
module main
fn main() {
doge_moon := '๐+๐=๐'
doge_moon_runes := doge_moon.runes()
println(doge_moon_runes)
println(typeof(doge_moon_runes).name) // []rune
}Count Sub String Occurences
In plain words: Calling .count('word') on a string tells you how many times that smaller word or letter appears inside the text.
What happens in this example:
- Prints the result to the terminal window so you can see the output right away.
module main
fn main() {
sp := 'The tiny tiger tied the tie tighter to its tail'
println(sp.count('t'))
// 10
println(sp.count('T'))
// 1
println(sp.count('tie'))
// 2
println(sp.count('-'))
// 0
}Check String Contains Substring
In plain words: Calling .contains('word') checks if a piece of text appears anywhere inside a string and returns true (yes) or false (no).
What happens in this example:
- Prints the result to the terminal window so you can see the output right away.
module main
fn main() {
hs := 'monday'
if hs.contains('mon') {
println('${hs} contains mon')
} else {
println('${hs} does not contains mon')
}
}String Contains Is Case Sensitive
In plain words: .contains(...) cares about uppercase vs lowercase letters ('V' is different from 'v'). If you want a case-insensitive check, convert the string with .to_lower() first!
What happens in this example:
- Prints the result to the terminal window so you can see the output right away.
module main
fn main() {
hs := 'Monday'
if hs.contains('mon') {
println('${hs} contains mon')
} else {
println('${hs} does not contains mon')
}
}Common String Methods
In plain words: V strings come packed with everyday text helpers like .to_upper(), .to_lower(), .trim_space(), .replace(), .starts_with(), and .ends_with().
What happens in this example:
- Code note: to_lower() returns the lowercase version of the string
- Code note: to_upper() returns the uppercase version of the string
- Code note: trim_space() trims leading and trailing whitespaces
- Code note: trim(cutset) trims leading and trailing characters that match any character in the cutset
module main
fn main() {
s := ' Hello, V! '
// to_lower() returns the lowercase version of the string
println(s.to_lower()) // " hello, v! "
// to_upper() returns the uppercase version of the string
println(s.to_upper()) // " HELLO, V! "
// trim_space() trims leading and trailing whitespaces
println(s.trim_space()) // "Hello, V!"
// trim(cutset) trims leading and trailing characters that match any character in the cutset
println(s.trim(' ')) // "Hello, V!"
// replace(old, new) replaces all occurrences of old with new
println(s.replace('V', 'World')) // " Hello, World! "
// replace_once(old, new) replaces the first occurrence of old with new
println(s.replace_once('l', 'x')) // " Hexlo, V! "
// index(sub) returns the start index of the first occurrence of sub as an optional ?int
idx := s.index('Hello') or { -1 }
println(idx) // 2
// last_index(sub) returns the start index of the last occurrence of sub as an optional ?int
last_idx := s.last_index('l') or { -1 }
println(last_idx) // 5
// starts_with(prefix) checks if the string starts with the prefix
println(s.starts_with(' ')) // true
// ends_with(suffix) checks if the string ends with the suffix
println(s.ends_with('!')) // false (ends with spaces)
// is_pure_ascii() checks if all characters in the string are pure ASCII
println(s.is_pure_ascii()) // true
// split_into_lines() splits a string into an array of lines
multiline := 'line 1\nline 2'
println(multiline.split_into_lines()) // ["line 1", "line 2"]
// split_by_space() splits a string by space as delimiter
println(s.split_by_space()) // ["Hello,", "V!"]
}Chapter 4 Control Flow
Plain-English Chapter Guide (Start Here if You Are New to Coding!)
- The Big Idea in Simple Words: Without control flow, a program would just run every line from top to bottom once and stop. Control flow gives your program a brain: it lets your code make decisions ("If it is raining, bring an umbrella; otherwise, wear sunglasses") and repeat tasks ("For every item on my shopping list, cross it off").
- Words to Know:
if/else if/else: A fork in the roadโruns a block of code only when a condition istrue.match: A clean multi-way switchboard that checks one value against many possible choices (much neater than writing tenelse iflines).forloop: Repeats a block of code multiple times (over a list of items, a range of numbers like0 .. 5, or while a condition stays true).breakandcontinue:breakexits a loop early;continueskips the rest of the current step and jumps to the next step of the loop.defer: Tells V: "Remember this cleanup job, and run it automatically right before this function finishes."
Quick Access โ Jump to any lesson in this chapter
Below is an index of all code examples in this chapter. You can use these links to jump directly to any specific code example:
Control Flow Extras
- Chaining Else If
- If With Goto
- Cascade Match Conditions
- Match As Switch Case
- Match Pattern Matching
- Match With Enum
- Match With Enum And Else
- Bare For
- Break For
- Continue For
- For C Style
- For On Array Without Index
- For On Arrays
- For On Maps
- For On Maps Ignore Key
- For On Range
- For With Continue Break And Labels
- Reverse For
Control flow determines the execution path of your code. In this chapter, we cover conditionals (if-else), pattern matching (match), and the versatile for loop. V simplifies control flow by using fewer keywords, making code highly readable.
Under the Hood: Control Flow Optimization, Jump Tables & Defer Stack
๐ง Optional Deep Dive โ Click to Expand / Collapse
Plain-English Summary: In simple words: V's if and match can directly return values to save you extra lines of code, and defer acts like an automatic checklist that runs your cleanup code in reverse order right as a function exits.
V provides clean, expressive control flow constructs that compile down to highly optimized machine code instructions.
`if` as an Expression
In V, if-else blocks can be used as expressions that evaluate to a value:
status := if score >= 50 { 'Pass' } else { 'Fail' }Under the hood, the V compiler transforms this into a C ternary operator (score >= 50 ? "Pass" : "Fail") or a direct conditional assignment on the stack. There is zero runtime overhead, no temporary object creation, and the compiler statically verifies that all branches return identical types.
`match` Compilation & Jump Tables
V's match statement replaces complex switch or cascading if-else chains:
* Exhaustiveness Guarantee: When matching on an enum or boolean, the compiler verifies that every single variant is handled. If a new enum variant is added in the future, the compiler will refuse to build until all match statements handle it.
* $O(1)$ Jump Table Optimization: When matching on contiguous integers or enums, the C backend compiles the match block into a hardware jump table. Dispatching to the target branch executes in $O(1)$ constant time regardless of how many branches exist.
* No Fallthrough: Unlike C/C++, V match branches do not fall through to the next case. Each branch is strictly isolated, completely eliminating the need for break statements.
The `defer` LIFO Execution Stack
The defer statement schedules a cleanup expression to execute right before the enclosing function exits:
fn process_file(path string) ! {
mut file := os.open(path)!
defer { file.close() } // Guaranteed to execute whenever process_file returns
// Perform operations...
}Under the hood:
- The compiler maintains a Last-In, First-Out (LIFO) stack of deferred expressions recorded within the function scope.
- At every possible exit point (including early
return,panic, or error propagation!), the compiler automatically injects the deferred cleanup code in reverse order. - This is achieved with zero runtime stack unwinding penaltyโthe injected calls are compiled directly into the function's epilogue.
Control Flow Extras
Chaining Else If
In plain words: When you have more than two choices, chain if ... else if ... else from top to bottom. V runs the first branch that is true and skips the rest.
When your program must choose between multiple mutually exclusive paths, you can chain multiple else if blocks together. V evaluates these conditions sequentially from top to bottom. As soon as one condition evaluates to true, the corresponding block of code is executed, and all remaining branches (including any final fallback else block) are skipped entirely. Like single if statements, parentheses are not required around the condition expressions in V.
This example defines a helper function that takes a weekday string and prints a corresponding breakfast menu using a chained conditional statement.
module main
// This helper chooses a meal plan based on the weekday.
fn breakfast_menu(day string) {
// Cascading conditional checks
if day == 'Monday' {
println('Bread, Jam, Half boiled Egg')
} else if day == 'Tuesday' {
println('Bread, Jam, Juice')
} else if day == 'Wednesday' {
println('Milk, Bread, Fruit Bowl')
} else if day == 'Thursday' {
println('Bread, Jam, Juice')
} else if day == 'Friday' {
println('Cereals, Bread, Jam, Half boiled Egg')
} else if day == 'Saturday' {
println('Milk, Bread, Fruit Bowl')
} else if day == 'Sunday' {
println('Cereals, Bread, Jam, Half boiled Egg')
} else {
// Fallback block executed if no prior conditions match
println('invalid input')
}
}
fn main() {
// Call the helper with a sample weekday.
breakfast_menu('Saturday')
}If With Goto
In plain words: unsafe { goto label } jumps directly to a named label in your code. You will almost never need goto in everyday programs (use loops and functions instead), but V includes it for rare low-level cleanup tasks.
What happens in this example:
- Prints the result to the terminal window so you can see the output right away.
module main
import os
fn main() {
improper_input_age:
println('Invalid input. Please provide value greater than 0.')
next_person:
inp := os.input('Enter your age:')
if inp != 'stop' {
age := inp.int()
if age >= 13 {
println('You are allowed to watch this movie')
} else if age > 0 && age < 13 {
println('Parental Guidance is required to watch this movie')
} else if age <= 0 {
unsafe {
goto improper_input_age
}
}
unsafe {
goto next_person
}
}
}Cascade Match Conditions
In plain words: In a match block, if several different values should run the exact same code, you can list them together separated by commas (like 'Monday', 'Tuesday' { ... }).
What happens in this example:
- Creates helper function(s)/method(s):
breakfast_menu. - Prints the result to the terminal window so you can see the output right away.
module main
fn breakfast_menu(day string) string {
return match day {
'Monday' {
'Bread, Jam, Half boiled Egg'
}
'Tuesday', 'Thursday' {
'Bread, Jam, Juice'
}
'Wednesday' {
'Milk, Bread, Fruit Bowl'
}
'Friday', 'Sunday' {
'Cereals, Bread, Jam, Half boiled Egg'
}
'Saturday' {
'Milk, Bread, Fruit Bowl'
}
else {
'invalid input'
}
}
}
fn main() {
friday_menu := breakfast_menu('Friday')
println(friday_menu)
sunday_menu := breakfast_menu('Sunday')
println(sunday_menu)
tuesday_menu := breakfast_menu('Tuesday')
println(tuesday_menu)
thursday_menu := breakfast_menu('Thursday')
println(thursday_menu)
}Match As Switch Case
In plain words: match is V's cleaner, safer version of if / else if: give it a value once at the top, and list each possible match case inside { ... }.
In V, there is no switch statement. Instead, the match keyword is used for branching on values. The match statement is highly readable and type-safe. Each branch is evaluated in order, and unlike in C/Java/Javascript, there is no "fall-through" behaviorโthe matching block executes and the statement completes immediately. This eliminates bugs caused by forgetting break statements. V also enforces that a match must cover all possible cases or provide an else block.
This example shows how to use match on string values.
module main
fn breakfast_menu(day string) {
// A match block branches on the value of 'day'.
// In V, match is an expression and can also return values.
match day {
'Monday' { println('Bread, Jam, Half boiled Egg') }
'Tuesday' { println('Bread, Jam, Juice') }
'Wednesday' { println('Milk, Bread, Fruit Bowl') }
'Thursday' { println('Bread, Jam, Juice') }
'Friday' { println('Cereals, Bread, Jam, Half boiled Egg') }
'Saturday' { println('Milk, Bread, Fruit Bowl') }
'Sunday' { println('Cereals, Bread, Jam, Half boiled Egg') }
else { println('invalid input') } // Exhaustive match requirement handled by 'else'
}
}
fn main() {
breakfast_menu('Sunday')
}Match Pattern Matching
In plain words: match can check not just single values, but also number ranges (like 1..10) to categorize values cleanly without writing complicated if conditions.
What happens in this example:
- Prints the result to the terminal window so you can see the output right away.
module main
fn main() {
age := 18
res := match age {
0...18 { 'Person with age ${age} classified as a Child' }
19...120 { 'Person with age ${age} classified as an Adult' }
else { '${age} is must be in the range 0 to 120' }
}
println(res)
}Match With Enum
In plain words: An enum defines a fixed set of named options (like Color.red, .green, .blue), and match makes sure you handle every option cleanly.
One of V's strongest safety guarantees is exhaustive enum matching. When you match on an enum value, V requires you to handle every single enum member. If you miss one, the compiler will refuse to compile the program. This ensures that when new items are added to an enum in the future, the compiler will automatically guide you to update all match expressions across your codebase. Additionally, V supports shorthand syntax where you can write .member_name instead of EnumName.member_name inside the match arms.
This example illustrates matching over an enum to return a string menu item.
module main
// Define a Days-of-the-week enum
enum Day {
sunday
monday
tuesday
wednesday
thursday
friday
saturday
}
fn breakfast_menu(day Day) string {
// The match statement returns the value of the matching block.
// Notice we use the shorthand dot syntax (.monday) since 'day' is known to be of type 'Day'.
return match day {
.monday {
'Bread, Jam, Half boiled Egg'
}
.tuesday, .thursday {
'Bread, Jam, Juice' // Grouping multiple enum members using comma
}
.wednesday {
'Milk, Bread, Fruit Bowl'
}
.friday, .sunday {
'Cereals, Bread, Jam, Half boiled Egg'
}
.saturday {
'Milk, Bread, Fruit Bowl'
}
} // No else block is needed because all enum members are exhaustively handled.
}
fn main() {
friday_menu := breakfast_menu(Day.friday)
println(friday_menu)
sunday_menu := breakfast_menu(Day.sunday)
println(sunday_menu)
tuesday_menu := breakfast_menu(Day.tuesday)
println(tuesday_menu)
thursday_menu := breakfast_menu(Day.thursday)
println(thursday_menu)
}Match With Enum And Else
In plain words: When matching on an enum (a fixed list of named choices), you can handle specific choices by name (.red, .green) and use else as a catch-all for any remaining choices.
What happens in this example:
- Creates helper function(s)/method(s):
weekend_breakfast_menu. - Prints the result to the terminal window so you can see the output right away.
module main
enum Day {
sunday
monday
tuesday
wednesday
thursday
friday
saturday
}
fn weekend_breakfast_menu(day Day) string {
return match day {
.sunday {
'Cereals, Bread, Jam, Half boiled Egg'
}
.saturday {
'Milk, Bread, Fruit Bowl'
}
else {
'Sorry, we are closed on weekdays!'
}
}
}
fn main() {
sunday_menu := weekend_breakfast_menu(Day.sunday)
println(sunday_menu)
tuesday_menu := weekend_breakfast_menu(Day.tuesday)
println(tuesday_menu)
}Bare For
In plain words: A for { ... } loop with no condition runs forever until it hits a break or return insideโgreat for game loops and servers!
V simplifies iteration by only offering a single keyword for loops: for. There is no while keyword in V. A "bare" for (a loop declaration without any conditions or loop ranges) represents an infinite loop, which behaves exactly like while true in other languages. To exit a bare for loop, you must use a break statement inside the loop body, or exit the function using return.
This example shows how to declare a bare infinite loop.
module main
fn main() {
mut count := 1
// Bare for starts an infinite loop.
// WARNING: Running this without a break condition will loop forever.
for {
println('Hi ${count} times')
count += 1
// To break out, a developer would normally add:
if count > 5 {
break
}
}
}Break For
In plain words: Inside a for loop, the break keyword acts like an emergency exitโit stops the loop immediately and jumps to the code after the loop.
What happens in this example:
- Uses
mutso the variable or collection is allowed to be updated after creation. - Prints the result to the terminal window so you can see the output right away.
module main
import os
fn main() {
mut count := 0
input := os.input('Enter number of times to Greet:')
limit := input.int()
for {
if count >= limit {
break
}
println('Hi')
count += 1
}
println('Greeted Hi ${count} times')
}Continue For
In plain words: Inside a for loop, the continue keyword says 'skip the rest of this turn and jump straight to the next item in the loop.'
What happens in this example:
- Prints the result to the terminal window so you can see the output right away.
module main
fn main() {
for i in 0 .. 10 {
if i % 2 == 0 { // skips printing number that is a multiple of 2
continue
}
println(i)
}
}For C Style
In plain words: A 3-part for loop (for i := 0; i < 10; i++) sets a starting counter (i := 0), checks a keep-going condition (i < 10), and steps the counter (i++) after each turn.
What happens in this example:
- Prints the result to the terminal window so you can see the output right away.
module main
fn main() {
sample := [3, 4, 23, 12, 4, 1, 45, 12, 42, 17, 92, 38]
for i := 0; i < sample.len; i += 3 {
println(sample[i])
}
}For On Array Without Index
In plain words: When you just want to look at each item in a list (array) without needing its position number, write for item in my_list { ... }.
What happens in this example:
- Prints the result to the terminal window so you can see the output right away.
module main
fn main() {
col := [1, 2, 3, 4, 5, 6, 7]
for val in col {
if val % 2 == 0 {
println('${val} is Even')
} else {
println('${val} is Odd')
}
}
}For On Arrays
In plain words: Write for index, item in my_array to loop through a list while getting both the item's position number (index) and its value (item).
Iterating over arrays is a very common requirement. In V, you can iterate over both the index and value of each element using the for index, value in array syntax. During each iteration, the index variable (e.g., idx) contains the zero-based array index, and the element variable (e.g., ele) contains a read-only copy of the item. Both variables are scoped exclusively to the body of the loop and cannot be mutated. If you only need the element values and not their indices, V allows you to omit the index variable (e.g. for value in array).
This example declares a list of fruits and iterates over them, printing each fruit alongside its corresponding index.
module main
fn main() {
fruits := ['apple', 'banana', 'coconut']
// Loop over the indices (idx) and elements (ele) of the fruits array
for idx, ele in fruits {
println('idx: ${idx} \t fruit: ${ele}')
}
}For On Maps
In plain words: Write for key, value in my_map to step through every key and its matching value in a dictionary map.
V makes iterating over key-value collections (maps) straightforward. By using the syntax for key, value in map, you can access both the key and the value of each entry directly during each iteration. Like arrays, the iteration variables (key and value) are local to the loop scope and are immutable. Note that iteration order over maps is not guaranteed to be stable or sorted, matching standard hash map behavior in other systems languages.
This example demonstrates how to declare a map and iterate over its key-value pairs.
module main
fn main() {
// Initialize a map with string keys and integer values
lottery := {
'First': 1000
'Second': 700
'Consolation': 200
}
// Loop over keys (k) and values (v) in the 'lottery' map.
for k, v in lottery {
println('${k} prize lottery amount: ${v}')
}
}For On Maps Ignore Key
In plain words: When looping over a map (dictionary), if you only care about the values and want to ignore the keys, use _ as a blank placeholder for the key: for _, value in my_map.
What happens in this example:
- Uses
mutso the variable or collection is allowed to be updated after creation. - Prints the result to the terminal window so you can see the output right away.
module main
fn main() {
basket := {
'apples': 10
'bananas': 12
}
mut total := 0
for _, v in basket {
total += v
}
println('Total number of fruits: ${total}')
}For On Range
In plain words: Writing for i in 1 .. 5 loops i through the numbers 1, 2, 3, 4 (stopping right before the upper number 5).
What happens in this example:
- Prints the result to the terminal window so you can see the output right away.
module main
fn main() {
for val in 0 .. 4 {
println(val)
}
}For With Continue Break And Labels
In plain words: When you have one loop nested inside another loop, you can put a name label (outer:) on the outside loop so break outer or continue outer can control the outer loop directly!
What happens in this example:
- Prints the result to the terminal window so you can see the output right away.
module main
import os
fn main() {
input := os.input('Enter the number of multiplication tables to print:')
limit := input.int()
if limit <= 0 {
return
}
first_loop: for i := 1; i <= 10; i++ {
println('Printing multiplication table for ${i}')
for j := 1; j <= 10; j++ {
mul := i * j
println('${i} * ${j} = ${mul}')
if mul >= limit * 10 {
break first_loop
}
}
println('*********')
}
}Reverse For
In plain words: By using a C-style for loop that starts at the end and counts down (i--), you can walk through numbers or array items backward.
What happens in this example:
- Prints the result to the terminal window so you can see the output right away.
module main
fn main() {
subjects := ['zoology', 'chemistry', 'physics', 'algebra']
for i := subjects.len - 1; i >= 0; i-- {
println(subjects[i])
}
}Chapter 5 Collections: Arrays and Maps
Plain-English Chapter Guide (Start Here if You Are New to Coding!)
- The Big Idea in Simple Words: What if you need to store 100 student names or look up a person's phone number by their name? Making 100 separate variables would be painful! V gives you two everyday organizers:
- An Array (
[]int,[]string) is an ordered numbered list (like a shopping list where item0is first, item1is second, and so on). - A Map (
map[string]int) is a lookup dictionary (like a phonebook where you look up a key like'Alice'to find its value like5551234).
- Words to Know:
- Array (
[10, 20, 30]): A list of items of the same type, numbered starting at index0. - Index (
arr[0]): The position number of an item in an array (computers start counting at0, soarr[0]is the first item). - Append (
<<): Pushes a new item onto the end of a mutable array. - Slice (
arr[1..3]): A view of a smaller piece of an array (from position 1 up to, but not including, position 3). - Map (
{'apple': 5}): A collection ofkey: valuepairs for instant lookup by key.
Quick Access โ Jump to any lesson in this chapter
Below is an index of all code examples in this chapter. You can use these links to jump directly to any specific code example:
Arrays
- Declare And Initialize
- Declare Empty Array
- Declare Array With Len
- Declare Array With Init And Len
- Declare Array With Cap
- Working With Array Properties
- Access Array Elements Using Index
- Access Array Elements Using Slices
- In Operator With Array
- Append Array
- Define Fixed Size Array
- Update Fixed Size Array Elements
- Determining Type Of Fixed Array
- Slicing Fixed Size Array Results In Ordinary Array
- Declaring Multi Dimensional Arrays
- Updating Multi Dimensional Array Indices
- Reassigning Multi Dimensional Arrays
- Clone Array
- Copy Array
- Sort Integer Array
- Sort String Array
- Sort Struct Array
- Filter Array
- Filter With Anonymous Funcs On Array
- Map Array Items
- Map Using Anonymous Funcs On Array
- Array Methods
Maps
Collections allow you to group multiple data items together. V provides two primary built-in collection types: arrays (ordered lists of elements) and maps (key-value dictionaries). This chapter covers creating, accessing, and manipulating these collections using modern functional patterns like map and filter.
Under the Hood: Dynamic Arrays, Slicing & Hash Table Mechanics
๐ง Optional Deep Dive โ Click to Expand / Collapse
Plain-English Summary: In simple words: A V array automatically doubles its reserved space when it gets full so adding items with << stays fast. Taking a slice (a[1..3]) shares a window into the original array unless you call .clone() to make a separate copy.
Arrays and maps form the backbone of data manipulation in V. Both are built for cache efficiency, fast iteration, and predictable memory growth.
Dynamic Array Architecture
Dynamic arrays in V are managed by a compact 24-byte control structure:
struct Array {
data voidptr // Pointer to continuous heap-allocated memory block
offset int // Slicing offset
len int // Number of active elements currently in array
cap int // Total capacity allocated in memory
element_size int // Size in bytes of each element (e.g., 4 for int)
}Array Header: [ data* | offset: 0 | len: 3 | cap: 4 | element_size: 4 ]
|
v
Heap Buffer: [ 10 ][ 20 ][ 30 ][ <unallocated> ]Amortized 2x Capacity Growth Strategy
When you append an item to an array using the << operator:
- If
len < cap, the element is written directly todata[len]andlenis incremented ($O(1)$ operation). - If
len == cap, V allocates a new memory block with double the previous capacity (cap * 2), copies the existing elements withmemcpy, frees the old buffer, and inserts the new item. - Pre-Allocation Best Practice: When the final count of items is known in advance, initialize the array with
[]int{cap: count}to allocate the exact memory buffer once, eliminating intermediate reallocation copies.
Slice Semantics: Shared Memory vs `.clone()`
When you create a slice using range syntax sub := arr[1..3]:
* The slice sub points directly to the memory buffer of the parent array arr.
* Creating a slice is an $O(1)$ operation with zero memory allocation.
* Caution: Modifying an element inside a mutable slice mutates the underlying data in the original array. If you need an isolated, independent copy, call arr[1..3].clone().
Map Architecture & The Wyhash Algorithm
V's built-in map[Key]Value is an open-addressing hash table:
* Hashing Engine: Uses Wyhash, one of the fastest hashing algorithms in existence, producing uniform hash distributions and high resilience against hash collision attacks.
* Cache Locality: Keys and values are stored in dense contiguous memory buckets, maximizing CPU L1/L2 cache hit rates during lookups.
* Lookup Cost: Key retrieval, insertion, and deletion operate in $O(1)$ average time complexity.
* Zero-Value Returns: Querying a missing key (m['absent']) returns the default zero-value of the value type (e.g. 0, "", false), or you can use key in m to check for explicit presence.
Arrays
Declare And Initialize
In plain words: To create a list (array) with starting items, wrap the items in square brackets separated by commas, like [1, 2, 3]. V automatically figures out the list's type from the items inside!
What happens in this example:
- Uses
mutso the variable or collection is allowed to be updated after creation. - Prints the result to the terminal window so you can see the output right away.
fn main() {
mut sports := ['cricket', 'hockey', 'football']
println(sports)
}Declare Empty Array
In plain words: To create an empty list (array) that you plan to fill later, write [] followed by the type of item it will hold and {} at the endโfor example, mut list := []int{}.
What happens in this example:
- Code note: prints empty array: []
- Code note: ['Chimpanzee', 'Dog']
fn main() {
mut animals := []string{}
println(animals)
// prints empty array: []
animals << 'Chimpanzee'
animals << 'Dog'
println(animals)
// ['Chimpanzee', 'Dog']
}Declare Array With Len
In plain words: Writing []int{len: 3} creates an array that already has 3 slots ready to go, filled with 0 by default ([0, 0, 0]).
What happens in this example:
- Uses
mutso the variable or collection is allowed to be updated after creation. - Prints the result to the terminal window so you can see the output right away.
fn main() {
mut i := []int{len: 3}
println(i)
}Declare Array With Init And Len
In plain words: Writing []int{len: 3, init: 1} creates an array of 3 slots and fills every slot with your chosen starting value (1) instead of 0, giving [1, 1, 1].
What happens in this example:
- Uses
mutso the variable or collection is allowed to be updated after creation. - Prints the result to the terminal window so you can see the output right away.
fn main() {
mut j := []int{len: 3, init: 1}
println(j)
}Declare Array With Cap
In plain words: Writing []int{cap: 2} creates an empty list (len: 0), but quietly reserves room in memory for 2 items ahead of time so appending items later is extra fast.
What happens in this example:
- Uses
mutso the variable or collection is allowed to be updated after creation. - Prints the result to the terminal window so you can see the output right away.
fn main() {
mut k := []int{cap: 2}
println(k)
}Working With Array Properties
In plain words: Every array has two built-in size properties: .len (how many items are currently in the list) and .cap (how many slots are currently reserved in memory before the array needs to grow).
What happens in this example:
- Code note: Length of sports array
- Code note: Capacity of sports array
- Code note: deleting football
fn main() {
mut sports := ['cricket', 'hockey', 'football']
println(sports.len)
// Length of sports array
println(sports.cap)
// Capacity of sports array
println('----Deleting football----')
sports.delete(2)
// deleting football
println('Length of sports array: ${sports.len}')
println('Capacity of sports array: ${sports.cap}')
println('----Adding volleyball and baseball----')
sports << ['volleyball', 'baseball']
println(sports)
println('Length of sports array: ${sports.len}')
println('Capacity of sports array: ${sports.cap}')
}Access Array Elements Using Index
In plain words: You read or change a specific item in an array using its position number (index) inside square brackets arr[index]. Remember: the first item is always at index 0!
What happens in this example:
- Uses
mutso the variable or collection is allowed to be updated after creation. - Prints the result to the terminal window so you can see the output right away.
fn main() {
mut sports := ['cricket', 'hockey', 'football']
s := sports[1]
println(s) // hockey
}Access Array Elements Using Slices
In plain words: A slice (arr[start..end]) grabs a range of items from an arrayโstarting at start and stopping right before end.
What happens in this example:
- Code note: Positive slicing: from index 1 to 3 (excluding index 3)
- Code note: V does not support negative indices natively in slices (e.g., sports[-2..] will not compile).
- Code note: To achieve "negative slicing" (indexing from the end of the array), use the
.lenproperty: - Code note: Slice up to the last element (excluding it): Python's sports[..-1]
fn main() {
mut sports := ['cricket', 'hockey', 'football', 'basketball', 'tennis']
// Positive slicing: from index 1 to 3 (excluding index 3)
println(sports[1..3]) // ['hockey', 'football']
// V does not support negative indices natively in slices (e.g., sports[-2..] will not compile).
// To achieve "negative slicing" (indexing from the end of the array), use the `.len` property:
// Slice up to the last element (excluding it): Python's sports[..-1]
println(sports[..sports.len - 1]) // ['cricket', 'hockey', 'football', 'basketball']
// Slice from 3rd to last up to 1st to last (excluding it): Python's sports[-3..-1]
println(sports[sports.len - 3..sports.len - 1]) // ['football', 'basketball']
// Slice the last two elements: Python's sports[-2..]
println(sports[sports.len - 2..]) // ['basketball', 'tennis']
// --- REFERENCE VS VALUE BEHAVIOR ---
// In V, slices are reference views of the original array.
// If you modify an element of a slice, it affects the original array.
// Note: To prevent unsafe behavior, assigning a slice to a variable requires an `unsafe` block
// if you want it by-reference, or an explicit `.clone()` to get a copy by-value.
// 1. Modifying by reference (using unsafe)
mut original := [10, 20, 30, 40, 50]
mut ref_slice := unsafe { original[1..4] }
ref_slice[0] = 99
println(original) // [10, 99, 30, 40, 50] (Original is modified!)
// 2. Modifying by value (using .clone())
mut original_two := [10, 20, 30, 40, 50]
mut val_slice := original_two[1..4].clone()
val_slice[0] = 99
println(original_two) // [10, 20, 30, 40, 50] (Original remains unchanged!)
}In Operator With Array
In plain words: Want to know if a value is inside an array? Just ask value in my_array (or value !in my_array for 'not in')โno manual loop required!
What happens in this example:
- Code note: prints: true
- Code note: prints: true
fn main() {
odd := [1, 3, 5, 7]
println(3 in odd)
// prints: true
println(8 !in odd)
// prints: true
}Append Array
In plain words: The push arrow << adds a new item (or even an entire list of items!) onto the end of a changeable (mut) array.
What happens in this example:
- Code note: prints [2, 4, 6, 8]
- Code note: prints: [2, 4, 6, 8, 10, 12, 14]
fn main() {
mut even := [2, 4, 6]
even << 8
println(even)
// prints [2, 4, 6, 8]
even << [10, 12, 14]
println(even)
// prints: [2, 4, 6, 8, 10, 12, 14]
}Define Fixed Size Array
In plain words: Putting a number inside the brackets ([3]int) creates a fixed-size array whose length is locked at 3 items forever. Because it never grows, V stores it directly in ultra-fast stack memory.
What happens in this example:
- Code note: [0, 0, 0, 0]
fn main() {
mut fix := [4]int{}
println(fix)
// [0, 0, 0, 0]
}Update Fixed Size Array Elements
In plain words: Even though a fixed-size array ([3]int) cannot grow or shrink in length, you can still change the individual values inside its slots if the variable is marked mut.
What happens in this example:
- Code note: [0, 33, 0, 0]
fn main() {
mut fix := [4]int{}
fix[1] = 33
println(fix)
//[0, 33, 0, 0]
}Determining Type Of Fixed Array
In plain words: In V, a fixed-size array's length is part of its type name (for example, [3]int is a distinct type from []int), which you can verify with typeof(arr).name.
What happens in this example:
- Uses
mutso the variable or collection is allowed to be updated after creation. - Prints the result to the terminal window so you can see the output right away.
fn main() {
mut fix := [4]int{}
println(typeof(fix).name) // [4]int
}Slicing Fixed Size Array Results In Ordinary Array
In plain words: If you take a slice of a fixed-size array (using arr[0..2] or arr[..]), V gives you back a normal dynamic array ([]int) so you can pass it to functions that expect standard arrays.
What happens in this example:
- Code note: [33, 0, 0]
fn main() {
mut fix := [4]int{}
fix[1] = 33
s := fix[1..]
println(s)
// [33, 0, 0]
println(typeof(s).name) // prints: []int
}Declaring Multi Dimensional Arrays
In plain words: A 2D or multi-dimensional array ([][]int) is simply a 'list of lists'โlike a grid, spreadsheet, or game board with rows and columns.
What happens in this example:
- Code note: [[0, 0], [0, 0], [0, 0], [0, 0]]
fn main() {
mut coordinates_2d := [][]int{len: 4, init: []int{len: 2}}
println(typeof(coordinates_2d).name)
// [][]int
println(coordinates_2d)
// [[0, 0], [0, 0], [0, 0], [0, 0]]
}Updating Multi Dimensional Array Indices
In plain words: In a 2D grid ([][]int), you access or change a single cell using two brackets: grid[row][column].
What happens in this example:
- Uses
mutso the variable or collection is allowed to be updated after creation. - Prints the result to the terminal window so you can see the output right away.
fn main() {
mut coordinates_2d := [][]int{len: 4, init: []int{len: 2}}
println(coordinates_2d.len)
point_1 := [0, 0]
point_2 := [0, 1]
point_3 := [1, 0]
point_4 := [1, 1]
coordinates_2d[0] = point_1
coordinates_2d[1] = point_2
coordinates_2d[2] = point_3
coordinates_2d[3] = point_4
println(coordinates_2d)
}Reassigning Multi Dimensional Arrays
In plain words: You can also replace an entire row of a 2D array at once by assigning a new 1D list to grid[row].
What happens in this example:
- Uses
mutso the variable or collection is allowed to be updated after creation. - Prints the result to the terminal window so you can see the output right away.
fn main() {
mut coordinates_2d := [][]int{len: 4, init: []int{len: 2}}
println(coordinates_2d.len)
coordinates_2d = [
[0, 0],
[0, 1],
[1, 0],
[1, 1],
]
println(coordinates_2d)
}Clone Array
In plain words: Assigning one array variable to another (b := a) can share the underlying memory. Calling .clone() creates a brand-new, independent copy so changing one list never affects the other.
What happens in this example:
- Code note: copies the array r to u
fn main() {
r := [1, 2, 3, 4]
mut u := r.clone()
// copies the array r to u
println(u)
}Copy Array
In plain words: Assigning one array variable to another (b := a) can share the underlying memory. Calling .clone() creates a brand-new, independent copy so changing one list never affects the other.
What happens in this example:
- Prints the result to the terminal window so you can see the output right away.
fn main() {
r := [1, 2, 3, 4]
s := unsafe { r }
println(s)
unsafe {
r.free()
}
}Sort Integer Array
In plain words: Calling .sort() on a changeable (mut) number array sorts it from smallest to largest (a < b). You can also sort from largest to smallest using .sort(a > b)!
What happens in this example:
- Code note: ascending order
- Code note: descending order
fn main() {
mut i := [3, 2, 8, 1]
i.sort()
// ascending order
println(i)
i.sort(a > b)
// descending order
println(i)
}Sort String Array
In plain words: Calling .sort() on a changeable (mut) list of strings puts them in alphabetical order (A to Z), or reverse alphabetical order with .sort(a > b).
What happens in this example:
- Code note: ascending order
- Code note: reverse order
fn main() {
mut fruits := ['Apples', 'avocado', 'banana', 'Orange']
fruits.sort()
// ascending order
println(fruits)
fruits.sort(a > b)
// reverse order
println(fruits)
}Sort Struct Array
In plain words: When you have a list of custom structs (like User), you tell .sort(...) which field to compareโfor example, users.sort(a.age < b.age) sorts users from youngest to oldest!
What happens in this example:
- Code note: Declare an empty array
- Code note: Create students
- Code note: Append all the students to the array
- Code note: Reverse Sort students by id
module main
struct Student {
id int
name string
class int
}
fn main() {
// Declare an empty array
mut students := []Student{}
// Create students
st1 := Student{
id: 1
name: 'Ram'
class: 9
}
st2 := Student{
id: 2
name: 'Katy'
class: 3
}
st3 := Student{
id: 3
name: 'Tom'
class: 6
}
// Append all the students to the array
students << [st1, st2, st3]
println(students)
// Reverse Sort students by id
students.sort(a.id > b.id)
println('Students sorted in reverse order of id:')
println(students)
// Sort students by class in ascending order
students.sort(a.class < b.class)
println('Students sorted in ascending order of class:')
println(students)
// Sort students by name in reverse order
students.sort(a.name > b.name)
println('Students sorted in reverse order of name:')
println(students)
}Filter Array
In plain words: .filter(...) creates a new list keeping only the items that pass your test. Inside .filter(...), the special variable it stands for the current item being checked (like nums.filter(it > 10)).
What happens in this example:
- Code note: [3, 6, 9]
fn main() {
f := [1, 2, 3, 4, 5, 6, 7, 8, 9]
multiples_of_3 := f.filter(it % 3 == 0)
println(multiples_of_3)
// [3, 6, 9]
}Filter With Anonymous Funcs On Array
In plain words: Instead of using it, you can pass a custom helper function (fn (x int) bool { ... }) into .filter(...) when your filtering rule needs more than a quick one-liner.
What happens in this example:
- Creates helper function(s)/method(s):
bool. - Prints the result to the terminal window so you can see the output right away.
fn main() {
fruits := ['apple', 'mango', 'water melon', 'musk melon']
fruits_starting_m := fruits.filter(fn (f string) bool {
return f.starts_with('m')
})
println(fruits_starting_m)
}Map Array Items
In plain words: .map(...) transforms every item in a list into something new (using it for each item)โfor example, nums.map(it * 2) returns a new list where every number is doubled!
What happens in this example:
- Prints the result to the terminal window so you can see the output right away.
fn main() {
visitor := ['Tom', 'Ram', 'Rao']
res := visitor.map('Mr. ' + it)
println(res)
}Map Using Anonymous Funcs On Array
In plain words: You can also pass an anonymous function (fn (x string) string { ... }) inside .map(...) to transform each item in an array step by step.
What happens in this example:
- Creates helper function(s)/method(s):
int. - Prints the result to the terminal window so you can see the output right away.
fn main() {
colors := ['red', 'blue', 'green', 'white', 'black']
colors_with_letter_e := colors.map(fn (c string) int {
if c.contains('e') { return 1 } else { return 0 }
})
println(colors_with_letter_e)
}Array Methods
In plain words: V arrays include convenient everyday methods like .reverse(), .first(), .last(), .pop(), .delete(), .insert(), .any(), and .all().
What happens in this example:
- Code note: A custom comparison function for sorting.
- Code note: It accepts references to elements (e.g. &int) and returns -1, 1, or 0.
- Code note: 1. ensure_cap(required)
- Code note: Ensures that the array has at least the specified capacity.
module main
// A custom comparison function for sorting.
// It accepts references to elements (e.g. &int) and returns -1, 1, or 0.
fn compare_ints(a &int, b &int) int {
val_a := *a
val_b := *b
if val_a < val_b {
return -1
}
if val_a > val_b {
return 1
}
return 0
}
fn main() {
println('--- Array Built-in Methods ---')
// 1. ensure_cap(required)
// Ensures that the array has at least the specified capacity.
mut a := [10, 20, 30]
a.ensure_cap(10)
println('ensure_cap: cap is ${a.cap >= 10}') // true
// 2. repeat(count)
// Repeats the array count times and returns a new array.
rep := a.repeat(2)
println('repeat: ${rep}') // [10, 20, 30, 10, 20, 30]
// 3. repeat_to_depth(count, depth) (unsafe)
// Recursively repeats a multi-dimensional array count times to the specified depth.
grid := [[1, 2], [3, 4]]
unsafe {
rep_grid := grid.repeat_to_depth(2, 1)
// Cast the raw array struct back to typed [][]int
typed_grid := *(&[][]int(&rep_grid))
println('repeat_to_depth: ${typed_grid}') // [[1, 2], [3, 4], [1, 2], [3, 4]]
rep_grid.free()
}
// 4. insert(index, val)
// Inserts a new element at the specified index.
a.insert(1, 15)
println('insert: ${a}') // [10, 15, 20, 30]
// 5. prepend(val)
// Prepends a new element at the beginning of the array.
a.prepend(5)
println('prepend: ${a}') // [5, 10, 15, 20, 30]
// 6. delete(index)
// Deletes the element at the specified index.
a.delete(1) // Deletes index 1 (which is 10)
println('delete: ${a}') // [5, 15, 20, 30]
// 7. delete_many(index, size)
// Deletes size elements starting from the specified index.
a.delete_many(1, 2) // Deletes 2 elements starting at index 1
println('delete_many: ${a}') // [5, 30]
// 8. clear()
// Sets the array length to 0, retaining capacity.
mut a_clear := [1, 2, 3]
a_clear.clear()
println('clear: len is ${a_clear.len}') // 0
// 9. reset() (unsafe)
// Sets all elements of the array to 0 / empty values without altering len or cap.
mut a_reset := [1, 2, 3]
unsafe {
a_reset.reset()
}
println('reset: ${a_reset}') // [0, 0, 0]
unsafe {
a_reset.free()
}
// 10. trim(index)
// Truncates the array length to index.
mut a_trim := [1, 2, 3, 4]
a_trim.trim(2)
println('trim: ${a_trim}') // [1, 2]
// 11. drop(num)
// Drops the first num elements in-place.
mut a_drop := [1, 2, 3, 4]
a_drop.drop(2)
println('drop: ${a_drop}') // [3, 4]
// 12. first()
// Returns the first element of the array.
println('first: ${a_drop.first()}') // 3
// 13. last()
// Returns the last element of the array.
println('last: ${a_drop.last()}') // 4
// 14. pop_left()
// Removes and returns the first element of the array.
mut a_pop := [1, 2, 3]
first_val := a_pop.pop_left()
println('pop_left: value = ${first_val}, array = ${a_pop}') // 1, [2, 3]
// 15. pop()
// Removes and returns the last element of the array.
last_val := a_pop.pop()
println('pop: value = ${last_val}, array = ${a_pop}') // 3, [2]
// 16. delete_last()
// Deletes the last element of the array.
mut a_del_last := [1, 2, 3]
a_del_last.delete_last()
println('delete_last: ${a_del_last}') // [1, 2]
// 17. clone()
// Returns a deep copy of the array.
a_clone := a_del_last.clone()
println('clone: ${a_clone}') // [1, 2]
// 18. clone_to_depth(depth) (unsafe)
// Recursively clones a multi-dimensional array up to the specified depth.
grid2 := [[1, 2], [3, 4]]
unsafe {
grid_clone := grid2.clone_to_depth(1)
typed_clone := *(&[][]int(&grid_clone))
println('clone_to_depth: ${typed_clone}') // [[1, 2], [3, 4]]
grid_clone.free()
}
// 19. push_many(val, size) (unsafe)
// Appends size elements starting from a raw pointer val to the array.
mut a_push := [1, 2]
vals := [3, 4]
unsafe {
a_push.push_many(vals.data, 2)
}
println('push_many: ${a_push}') // [1, 2, 3, 4]
unsafe {
a_push.free()
vals.free()
}
// 20. reverse()
// Returns a new reversed copy of the array.
a_rev := [1, 2, 3]
println('reverse: ${a_rev.reverse()}') // [3, 2, 1]
// 21. reverse_in_place()
// Reverses the array elements in-place.
mut a_rev_ip := [1, 2, 3]
a_rev_ip.reverse_in_place()
println('reverse_in_place: ${a_rev_ip}') // [3, 2, 1]
// 22. free() (unsafe)
// Deallocates the array's buffer.
mut a_free := [1, 2, 3]
unsafe {
a_free.free()
}
println('free: array freed')
// 23. filter(it)
// Filters elements that satisfy a predicate using compiler-defined `it` expression.
a_filt := [1, 2, 3, 4]
filtered := a_filt.filter(it % 2 == 0)
println('filter: ${filtered}') // [2, 4]
// 24. any(it)
// Checks if any element satisfies the predicate.
println('any: ${a_filt.any(it > 3)}') // true
// 25. count(it)
// Counts how many elements satisfy the predicate.
println('count: ${a_filt.count(it % 2 == 0)}') // 2
// 26. all(it)
// Checks if all elements satisfy the predicate.
println('all: ${a_filt.all(it > 0)}') // true
// 27. map(it)
// Maps elements to a new array using a transformation expression.
mapped := a_filt.map(it * 10)
println('map: ${mapped}') // [10, 20, 30, 40]
// 28. sort() & sort(custom)
// Sorts elements in-place. Uses optional boolean expression for custom order (uses magic vars a and b).
mut a_sort := [3, 1, 4, 2]
a_sort.sort()
println('sort (default ascending): ${a_sort}') // [1, 2, 3, 4]
a_sort.sort(a > b)
println('sort (custom descending): ${a_sort}') // [4, 3, 2, 1]
// 29. sorted() & sorted(custom)
// Returns a sorted copy of the array. Uses optional boolean expression for custom order (uses magic vars a and b).
a_sorted := [3, 1, 4, 2]
println('sorted (default): ${a_sorted.sorted()}') // [1, 2, 3, 4]
println('sorted (custom): ${a_sorted.sorted(a > b)}') // [4, 3, 2, 1]
// 30. sort_with_compare(callback)
// Sorts the array in-place using a custom comparison function.
mut a_compare := [3, 1, 4, 2]
a_compare.sort_with_compare(compare_ints)
println('sort_with_compare: ${a_compare}') // [1, 2, 3, 4]
// 31. sorted_with_compare(callback)
// Returns a sorted copy of the array using a custom comparison function.
a_sorted_comp := [3, 1, 4, 2]
println('sorted_with_compare: ${a_sorted_comp.sorted_with_compare(compare_ints)}') // [1, 2, 3, 4]
// 32. contains(value)
// Checks if the array contains value.
println('contains: ${a_filt.contains(3)}') // true
// 33. index(value)
// Returns the index of the first occurrence of value, or -1 if not found.
println('index: ${a_filt.index(3)}') // 2
// 34. last_index(value)
// Returns the index of the last occurrence of value, or -1 if not found.
a_dup := [1, 2, 3, 2]
println('last_index: ${a_dup.last_index(2)}') // 3
// 35. grow_cap(amount)
// Increases the array capacity by the specified amount.
mut a_grow := [1, 2]
a_grow.grow_cap(10)
println('grow_cap: cap is ${a_grow.cap >= 12}') // true
// 36. grow_len(amount) (unsafe)
// Increases the array length by the specified amount.
unsafe {
a_grow.grow_len(3)
}
println('grow_len: ${a_grow}') // [1, 2, 0, 0, 0]
unsafe {
a_grow.free()
}
// 37. pointers() (unsafe)
// Returns an array of void pointers (pointers()) pointing to each element.
a_ptrs := [10, 20]
unsafe {
ptrs := a_ptrs.pointers()
println('pointers (first element): ${*(&int(ptrs[0]))}') // 10
ptrs.free()
a_ptrs.free()
}
}Array Update Syntax
In plain words: Using [...old_array, new_item] creates a new array that copies all items from old_array and tacks on additional items.
V lets you initialise an array by spreading an existing array (using ellipsis spread syntax ...), optionally followed by additional elements.
Spreading is written as [...base, 3, 4]. This creates a new modified version of the array without mutating the original variable.
module main
fn main() {
println('=== Array Update Syntax ===')
base := [1, 2]
a := [...base, 3, 4]
println('base: ${base}') // [1, 2]
println('a: ${a}') // [1, 2, 3, 4]
assert base == [1, 2]
assert a == [1, 2, 3, 4]
}Maps
Explicit Map Initialization
In plain words: To create an empty map (key-value dictionary), write map[KeyType]ValueType{}โfor example, mut ages := map[string]int{} maps text names (string) to whole-number ages (int).
What happens in this example:
- Uses
mutso the variable or collection is allowed to be updated after creation. - Prints the result to the terminal window so you can see the output right away.
fn main() {
mut books := map[string]int{}
books['V on Wheels'] = 320
books['Go for Dummies'] = 279
println(books)
}Short Syntax Initialization Of Map
In plain words: When you already know the starting key-value pairs, you can create a map with the short { 'key': value } syntaxโjust like a mini dictionary!
What happens in this example:
- Uses
mutso the variable or collection is allowed to be updated after creation. - Prints the result to the terminal window so you can see the output right away.
fn main() {
mut student_1 := {
'english': 90
'mathematics': 96
'physics': 83
'chemistry': 89
}
println(student_1)
}Count Key Value Pairs In Map
In plain words: Checking .len on a map tells you how many key: value pairs are currently stored inside it.
What happens in this example:
- Uses
mutso the variable or collection is allowed to be updated after creation. - Prints the result to the terminal window so you can see the output right away.
fn main() {
mut student_1 := {
'english': 90
'mathematics': 96
'physics': 83
'chemistry': 89
}
cnt := student_1.len
println('There are ${cnt} key-value pairs in student_1 map')
}Value Given Key Of Map
In plain words: You look up a value in a map by putting its key inside square brackets: my_map['key'].
What happens in this example:
- Uses
mutso the variable or collection is allowed to be updated after creation. - Prints the result to the terminal window so you can see the output right away.
fn main() {
mut student_1 := {
'english': 90
'mathematics': 96
'physics': 83
'chemistry': 89
}
println(student_1['physics']) // 83
}Value Given Non Existent Key Of Map
In plain words: If you look up a key that isn't in the map yet, V safely gives you the default zero/empty value for that type (0 for numbers, '' for strings) instead of crashing!
What happens in this example:
- Uses
mutso the variable or collection is allowed to be updated after creation. - Prints the result to the terminal window so you can see the output right away.
fn main() {
mut student_1 := {
'english': 90
'mathematics': 96
'physics': 83
'chemistry': 89
}
println(student_1['geography']) // 0
}Handling Missing Keys In Map
In plain words: To tell the difference between a key whose value is 0 and a key that doesn't exist at all, use key in my_map or attach an or { ... } block to my_map[key]!
What happens in this example:
- Uses
mutso the variable or collection is allowed to be updated after creation. - Includes an
or { ... }safety block to handle potential errors or missing values calmly.
fn main() {
mut student_1 := {
'english': 90
'mathematics': 96
'physics': 83
'chemistry': 89
}
sub := 'geography'
res := student_1[sub] or { panic('marks for subject ${sub} not yet updated') } // throws error
}Update Value Given A Key In Map
In plain words: On a changeable (mut) map, my_map['key'] = new_value adds the key if it is new, or updates its value if the key is already there.
What happens in this example:
- Uses
mutso the variable or collection is allowed to be updated after creation. - Prints the result to the terminal window so you can see the output right away.
fn main() {
mut student_1 := {
'english': 90
'mathematics': 96
'physics': 83
'chemistry': 89
}
student_1['english'] = 93
println(student_1)
}Delete Key Value Pair From Map
In plain words: Calling my_map.delete('key') removes that key and its value from a changeable (mut) map.
What happens in this example:
- Uses
mutso the variable or collection is allowed to be updated after creation. - Prints the result to the terminal window so you can see the output right away.
fn main() {
mut student_1 := {
'english': 90
'mathematics': 96
'physics': 83
'chemistry': 89
}
println('Key-Value pairs before deleting a key: ${student_1.len}')
student_1.delete('physics')
println('Key-Value pairs after deleting a key ${student_1.len}')
}Map Methods
In plain words: Maps provide helpful methods like .keys() (get a list of all keys), .values() (get a list of all values), .clone() (make an independent copy), and .clear().
What happens in this example:
- Code note: 1. keys()
- Code note: Returns an array containing all keys in the map.
- Code note: 2. values()
- Code note: Returns an array containing all values in the map.
module main
fn main() {
println('--- Map Built-in Methods ---')
mut m := {
'one': 1
'two': 2
}
println('initial map: ${m}') // {"one": 1, "two": 2}
// 1. keys()
// Returns an array containing all keys in the map.
println('keys: ${m.keys()}') // ["one", "two"]
// 2. values()
// Returns an array containing all values in the map.
println('values: ${m.values()}') // [1, 2]
// 3. clone()
// Returns a deep copy of the map.
mut m_clone := m.clone()
println('clone: ${m_clone}') // {"one": 1, "two": 2}
// 4. delete(key)
// Removes a key-value pair from the map by key.
m.delete('one')
println('delete: ${m}') // {"two": 2}
// 5. reserve(capacity)
// Pre-allocates space for at least capacity elements in the map.
m.reserve(10)
println('reserve: reserved capacity successfully')
// 6. clear()
// Removes all key-value pairs from the map without deallocating data.
m.clear()
println('clear: len is ${m.len}') // 0
// 7. move()
// Moves the map contents to a new map variable and clears the original map to empty.
mut m_move := {
'three': 3
'four': 4
}
moved := m_move.move()
println('move (new map): ${moved}') // {"three": 3, "four": 4}
println('move (original map): ${m_move}') // {}
// 8. free() (unsafe)
// Deallocates the map memory.
mut m_free := {
'temp': 100
}
unsafe {
m_free.free()
}
println('free: map freed successfully')
}Import Maps Helpers
In plain words: The standard maps module (import maps) adds extra power tools for filtering, transforming (to_array), inverting, and merging maps.
The maps module provides higher-level helpers for filtering, transforming, inverting, merging, and converting between maps and arrays. These helpers are useful when you want to work with map data in a functional style without manually writing the loops yourself.
module main
import maps
fn main() {
println('--- Import Maps Module Helpers ---')
// Start with a simple map of fruit counts.
m1 := {
'apple': 1
'banana': 2
'cherry': 3
}
// filter() keeps only the entries that satisfy the callback condition.
filtered := maps.filter(m1, fn (k string, v int) bool {
return v > 1
})
println('filter(): ${filtered}')
// to_array() builds a new array by transforming each entry.
keys_upper := maps.to_array(m1, fn (k string, v int) string {
return k.to_upper()
})
println('to_array(): ${keys_upper}')
// invert() swaps each key/value pair so the values become the keys.
inverted := maps.invert(m1)
println('invert(): ${inverted}')
// from_array() creates a map from a list of strings.
fruits := ['apple', 'banana', 'cherry']
map_from_arr := maps.from_array(fruits)
println('from_array(): ${map_from_arr}')
// merge() combines two maps and lets the second map override duplicates.
m2 := {
'banana': 20
'date': 4
}
merged := maps.merge(m1, m2)
println('merge(): ${merged}')
// merge_in_place() mutates the first map directly.
mut mut_map := {
'a': 1
}
maps.merge_in_place(mut mut_map, {
'b': 2
'c': 3
})
println('merge_in_place(): ${mut_map}')
// flat_map() can expand each entry into multiple output values.
flat_items := maps.flat_map[string, int, string](m1, fn (k string, v int) []string {
return [k, v.str()]
})
println('flat_map(): ${flat_items}')
// to_map() transforms each entry into a new key/value pair.
transformed := maps.to_map[string, int, string, int](m1, fn (k string, v int) (string, int) {
return k.to_upper(), v * 10
})
println('to_map(): ${transformed}')
}Map Update Syntax
In plain words: Using { ...old_map, 'key': new_val } creates a fresh copy of old_map with specific keys added or updated.
Similar to structs, V lets you initialise a new map with updates applied on top of an existing map using the spread syntax .... This is functionally equivalent to cloning the map and updating it, except that it avoids declaring a mutable intermediate variable and allows inlining elements.
module main
fn main() {
println('=== Map Update Syntax ===')
base_map := {
'a': 4
'b': 5
}
// Create a new map by updating elements of base_map
foo := {
...base_map
'b': 88
'c': 99
}
println('base_map: ${base_map}') // {'a': 4, 'b': 5}
println('foo: ${foo}') // {'a': 4, 'b': 88, 'c': 99}
assert base_map['b'] == 5
assert foo['a'] == 4
assert foo['b'] == 88
assert foo['c'] == 99
}Chapter 6 Functions
Plain-English Chapter Guide (Start Here if You Are New to Coding!)
- The Big Idea in Simple Words: A function (
fn) is a reusable mini-recipe with a name. Instead of copying and pasting the same 10 lines of code everywhere, you put those lines inside a function once, give it inputs (parameters), and ask it to hand back an answer (return value) whenever you need it. - Words to Know:
- Function (
fn add(a int, b int) int): A named block of code you can call whenever you want. - Parameters / Arguments: The inputs you feed into a function inside
(...). - Return Value (
return a + b): The answer the function sends back after it finishes its job. - Anonymous Function / Closure: A quick, nameless function you can store in a variable or pass into another function as a helper.
- Variadic (
...int): A function parameter that accepts any number of inputs (one, three, or ten!).
Quick Access โ Jump to any lesson in this chapter
Below is an index of all code examples in this chapter. You can use these links to jump directly to any specific code example:
Advanced Function Features
- Function Returns Value Example 1
- Function Returns Value Example 2
- Funtions Without Return Type
- Function With Input Arguments
- Function Return Multiple Values
- Ignore Function Return Value
- Function Calls Other Function
- Example 1
- Example 2
- Error Script Functions
- Script Functions
- Functions Module Variables - Main (main.v)
- Mymod
- Functions With Optional Return Types Example 1
- Function With Optional Return Type Example 2
- Mod1
- Public Function Demo1
- Public Function Demo2
- Public Function Demo3
- Function With Defer Block
- Functions As Elements Of Array Or Map
Function Extras
Functions let you turn repeated or complex logic into small, named building blocks. A useful mental model is: define the task, give it inputs if needed, do the work, and return a useful result. This chapter starts with simple functions and then introduces multiple returns, optional results, higher-order functions, and cleanup with defer.
Under the Hood: Calling Conventions, Multiple Returns & Closures
๐ง Optional Deep Dive โ Click to Expand / Collapse
Plain-English Summary: In simple words: V functions can return multiple values at once without slowing down your computer, and they only allow a function to modify an input variable if both the function definition and the caller explicitly write mut.
Functions in V are designed for clarity, zero runtime overhead, and seamless integration with native C calling conventions.
Multiple Return Values Without Heap Allocation
In many languages, returning multiple values requires allocating a tuple or object on the heap. In V:
fn divide(a int, b int) (int, int) {
return a / b, a % b
}Under the hood, the compiler creates an anonymous C struct on the caller's stack frame:
typedef struct { int arg0; int arg1; } MultiReturn_int_int;The return values are written directly into this stack structure or returned via CPU registers, resulting in zero heap allocation and optimal performance.
Parameter Passing Conventions
* Pass-by-Value (Default): Primitive types (int, f64, bool) and small structs are passed by value (copied onto the stack or passed directly in CPU registers).
* Pass-by-Reference (mut): When a parameter is declared with mut (e.g. fn update(mut user User)), V passes a pointer to the caller's instance (User*), allowing in-place modifications.
* Immutable Parameters: Function parameters are immutable by default. You cannot reassign or modify a parameter unless explicitly marked mut.
Closures & Environment Capture
When an anonymous function references variables from its enclosing scope:
multiplier := 3
triple := fn [multiplier] (x int) int {
return x * multiplier
}The V compiler generates a hidden environment closure struct that captures the referenced variable (multiplier), ensuring safe access across scope boundaries.
Function Attributes
| Attribute | Purpose | Description |
|---|---|---|
@[inline] | Performance | Instructs the backend C compiler to inline the function body directly at callsites, eliminating function call overhead in performance-critical loops. |
@[params] | Clean API Design | Enables the struct options pattern, allowing callers to pass named arguments or omit optional parameters cleanly. |
@[deprecated: 'Use new_fn()'] | Maintainability | Emits a compiler warning whenever the marked function is called. |
Advanced Function Features
Function Returns Value Example 1
In plain words: When a function calculates an answer, put the answer's type after the (...) parameters and use return to hand that value back to the caller.
A simple function is a small helper that turns inputs into a useful result. The basic pattern is: define the function, pass in values, do some work, and return the answer. In this example, add takes two integers and returns their sum.
fn add(a int, b int) int {
return a + b
}
fn main() {
println(add(2, 3))
}Function Returns Value Example 2
In plain words: A function can check conditions and return early with an answer as soon as its work is done.
A function does not need to print anything itself. It can build a value and hand it back to the caller. Here, say_hello returns a greeting string, and main decides how to display it.
What happens in this example:
- Code note: call the method
- Code note: prints: Hello!
fn say_hello() string {
return 'Hello!'
}
fn main() {
// call the method
res := say_hello()
println(res)
// prints: Hello!
}Functions Without Return Type
In plain words: If a function just performs an action (like printing a message) and doesn't need to hand back a value, simply leave off the return type after (...).
Some functions are used for actions rather than calculations. They may print output, write files, or update state. In that case, you can leave out the return type and focus on the side effect.
What happens in this example:
- Code note: prints: Hello!
fn console_greeter() {
println('Hello!')
}
fn main() {
console_greeter()
// prints: Hello!
}Function With Input Arguments
In plain words: You pass inputs (arguments) into a function by listing each input's name and type inside the parentheses: fn add(x int, y int) int.
Functions become much more useful when they accept inputs. This example uses two numbers as arguments and returns their sum, showing the classic input โ process โ output flow.
What happens in this example:
- Code note: prints: 6
fn add(a int, b int) int {
return a + b
}
fn main() {
res := add(2, 4)
println(res)
// prints: 6
}Function Return Multiple Values
In plain words: In V, a single function can return two or more values at once by wrapping the return types in parentheses, like fn stats() (int, int).
In V, functions are not limited to returning a single value. A function can return a tuple containing two or more values when they logically belong together. A very common pattern in systems programming is returning both the main result of an operation and a secondary value (such as status flags, byte counts, or errors). Returning multiple values is clean, avoids wrapping results in temporary struct containers, and is assigned using parallel declaration at the call site.
This example shows how a function computes a string greeting and returns both the greeting string and its length as an integer.
// The return types are declared within parentheses: (string, int)
fn greet_and_message_length(name string) (string, int) {
mut greeting := 'Hello, ' + name + '!'
// Multiple values are returned as a comma-separated list
return greeting, greeting.len
}
fn main() {
// Receive multiple return values using parallel assignment
i, j := greet_and_message_length('Navule')
println(i) // Prints: Hello, Navule!
println(j) // Prints the length: 14
}Ignore Function Return Value
In plain words: When a function returns multiple values and you only need one of them, put an underscore _ in place of the value you want to ignore: first, _ := get_pair().
Sometimes you only care about one returned value. V lets you ignore the rest with _, which keeps the code readable when you are only interested in part of the result.
What happens in this example:
- Creates helper function(s)/method(s):
greet_and_message_length. - Uses
mutso the variable or collection is allowed to be updated after creation. - Prints the result to the terminal window so you can see the output right away.
fn greet_and_message_length(name string) (string, int) {
mut greeting := 'Hello, ' + name + '!'
return greeting, greeting.len
}
fn main() {
i, _ := greet_and_message_length('Navule')
println(i)
}Function Calls Other Function
In plain words: Big problems become easy when you break them into small helper functions that call each other step by step.
Functions can call other functions to split a bigger problem into smaller steps. This makes the code easier to understand and easier to reuse later.
What happens in this example:
- Creates helper function(s)/method(s):
greet, welcome. - Uses
mutso the variable or collection is allowed to be updated after creation. - Prints the result to the terminal window so you can see the output right away.
fn greet(p string) string {
return 'Hello, ${p}!'
}
fn welcome(p string) string {
msg := 'Nice to meet you!'
mut g := greet(p)
g = g + ' ${msg}'
return g
}
fn main() {
res := welcome('Visitor')
println(res)
}Example 1
In plain words: You can pass any V typeโnumbers, strings, arrays, maps, structs, or even other functionsโinto a function as an input parameter.
What happens in this example:
- Creates helper function(s)/method(s):
increment_array_items. - Uses
mutso the variable or collection is allowed to be updated after creation. - Prints the result to the terminal window so you can see the output right away.
fn increment_array_items(arr []int, inc int) []int {
mut tmp := arr.clone()
for mut i in tmp {
i += inc
}
return tmp
}
fn main() {
a := [5, 6]
res := increment_array_items(a, 100)
println('a: ${a}')
println('res: ${res}')
}Example 2
In plain words: You can pass any V typeโnumbers, strings, arrays, maps, structs, or even other functionsโinto a function as an input parameter.
What happens in this example:
- Code note: Must specify mut keyword when sending value to mut arg of a function
fn increment_array_items(mut arr []int, inc int) {
for mut i in arr {
i += inc
}
}
fn main() {
mut a := [5, 6]
increment_array_items(mut a, 100)
// Must specify mut keyword when sending value to mut arg of a function
println('a: ${a}')
}Error Script Functions
In plain words: In V shell scripts (.vsh files), any helper functions you write (fn ...) should be placed before the top-level script statements that call them.
What happens in this example:
- Creates helper function(s)/method(s):
log. - Prints the result to the terminal window so you can see the output right away.
#!/usr/local/bin/v run
cnt := 2
for i in 0 .. cnt {
log('iteration ${i}')
}
fn log(msg string) {
println(msg)
}Script Functions
In plain words: In V shell scripts (.vsh files), any helper functions you write (fn ...) should be placed before the top-level script statements that call them.
What happens in this example:
- Creates helper function(s)/method(s):
log. - Prints the result to the terminal window so you can see the output right away.
#!/usr/local/bin/v run
fn log(msg string) {
println(msg)
}
cnt := 2
for i in 0 .. cnt {
log('iteration ${i}')
}Functions Module Variables - Main (main.v)
In plain words: Because V avoids hidden global variables, functions share fixed values across a module using module-level const blocks or by passing structs/values as function inputs.
What happens in this example:
- Code note: file: main.v
// file: main.v
module main
import mymod
fn main() {
mymod.msg := 'global variable demo'
println(mymod.msg)
}Mymod
In plain words: Because V avoids hidden global variables, functions share fixed values across a module using module-level const blocks or by passing structs/values as function inputs.
What happens in this example:
- Code note: file: mymod/mymod.v
// file: mymod/mymod.v
module mymod
__global (
msg string
)Functions With Optional Return Types Example 1
In plain words: Putting ? in front of a function's return type (like ?int or ?string) means the function might return a real value OR none if nothing was found.
What happens in this example:
- Creates helper function(s)/method(s):
is_teen. - Includes an
or { ... }safety block to handle potential errors or missing values calmly. - Prints the result to the terminal window so you can see the output right away.
module main
fn is_teen(age int) ?string {
if age < 0 {
return none
} else if age >= 13 && age <= 19 {
return 'teenager'
} else {
return 'not teenager'
}
}
fn main() {
x := is_teen(-3) or { 'invalid age provided' }
println(x)
}Function With Optional Return Type Example 2
In plain words: Putting ? in front of a function's return type (like ?int or ?string) means the function might return a real value OR none if nothing was found.
What happens in this example:
- Creates helper function(s)/method(s):
is_teen. - Includes an
or { ... }safety block to handle potential errors or missing values calmly. - Prints the result to the terminal window so you can see the output right away.
module main
fn is_teen(age int) !string {
if age < 0 {
return error('invalid age provided')
} else if age >= 13 && age <= 19 {
return 'teenager'
} else {
return 'not teenager'
}
}
fn main() {
x := is_teen(-3) or { err.msg() }
println(x)
}Mod1
In plain words: Functions without pub can only be called by files in the same folder. Adding pub fn unlocks the function so other modules that import this folder can call it.
What happens in this example:
- Code note: file: mod1/mod1.v
// file: mod1/mod1.v
module mod1
fn greet1() string {
return 'Hello from greet1'
}
pub fn greet2() string {
return 'Hello from greet2'
}
pub fn greet_and_wish() string {
wish := 'Have a nice day!'
return greet1() + ', ' + wish
}Public Function Demo1
In plain words: Functions without pub can only be called by files in the same folder. Adding pub fn unlocks the function so other modules that import this folder can call it.
What happens in this example:
- Code note: file: publicfunctiondemo1.v
// file: public_function_demo1.v
import mod1
fn main() {
g := mod1.greet1()
println(g)
}Public Function Demo2
In plain words: Functions without pub can only be called by files in the same folder. Adding pub fn unlocks the function so other modules that import this folder can call it.
What happens in this example:
- Code note: file: publicfunctiondemo2.v
// file: public_function_demo2.v
import mod1
fn main() {
g := mod1.greet2()
println(g)
}Public Function Demo3
In plain words: Functions without pub can only be called by files in the same folder. Adding pub fn unlocks the function so other modules that import this folder can call it.
What happens in this example:
- Code note: file: publicfunctiondemo3.v
// file: public_function_demo3.v
import mod1
fn main() {
g := mod1.greet_and_wish()
println(g)
}Function With Defer Block
In plain words: Code inside a defer { ... } block waits patiently and runs automatically right when the current function finishesโperfect for closing files or connections so you never forget.
The defer keyword is a crucial feature for resource safety and cleanups. A defer block schedules a block of code to run automatically right before the containing function exits, regardless of which return path is taken. If there are multiple defer blocks in a function, they are executed in reverse order of their declaration (Last-In, First-Out). This ensures resources (like file descriptors, database connections, or socket connections) are closed safely without duplicating cleanup code across every return statement.
This example illustrates the execution sequence of statements inside a function containing a defer block.
module main
fn void_func_defer() {
println('Hello')
// This block is scheduled to execute at the very end of the function scope.
defer {
println('Hi from defer block')
}
println('How are you?')
// The function ends here, triggering the deferred block execution automatically.
}
fn main() {
void_func_defer()
}Functions As Elements Of Array Or Map
In plain words: In V, functions are first-class values: you can store functions inside an array or a map and call them dynamically!
Functions can be stored in arrays and maps just like other values. This allows you to choose an operation dynamically at runtime, which is helpful in flexible programs.
What happens in this example:
- Creates helper function(s)/method(s):
adder, subtractor, multiplier. - Prints the result to the terminal window so you can see the output right away.
module main
fn adder(i int, j int) int {
return i + j
}
fn subtractor(i int, j int) int {
return i - j
}
fn multiplier(i int, j int) int {
return i * j
}
fn main() {
i, j := 2, 5
println('Functions as elements of an Array')
funcs := [adder, subtractor, multiplier]
for f in funcs {
res := f(i, j)
println(res)
}
println('Functions as elements of Map')
d := {
'sum': adder
'difference': subtractor
'product': multiplier
}
for key, val in d {
res := val(i, j)
println('${key} of ${i} and ${j}: ${res}')
}
}Function Extras
Hello
In plain words: fn main() is the front door of every V programโwhen you run your app, execution always starts right here.
Every V program starts with main(). It is the entry point where execution begins, so it is the first function most beginners learn.
What happens in this example:
- Prints the result to the terminal window so you can see the output right away.
module main
fn main() {
println('Welcome to the World of V!')
}Basic Functions
In plain words: Shows the basic building blocks of declaring and calling reusable functions in a single V file.
A basic function packages a task so you can call it later instead of repeating the same code. This example shows a function that prints a message when invoked.
What happens in this example:
- Code note: Define a simple function that prints a greeting.
- Code note: Call the function with different argument values.
// Define a simple function that prints a greeting.
fn greet(name string) {
println('Hello, ${name}!')
}
fn main() {
// Call the function with different argument values.
greet('Ada')
greet('Grace')
}Anonymous Functions
In plain words: An anonymous function is a function without a name (fn (a int) int { ... }) that you can save inside a variable and call whenever you need it.
Anonymous functions (also known as lambda functions or function literals) are functions that are defined inline without a name. In V, functions are first-class citizens, meaning they can be assigned to variables, passed as arguments to other functions, or returned from functions. Anonymous functions are highly useful for short-lived, one-off operations such as custom sort criteria, filter callbacks, or event handlers.
This example shows how to declare an anonymous function, assign it to a variable, and call it.
module main
fn main() {
// Define an anonymous function using the 'fn' keyword without a name.
// Assign it directly to the local variable 'greet'.
greet := fn (name string) {
println('Hello, ${name}')
}
// Invoke the anonymous function by calling the variable as if it were a named function.
greet('Pavan')
greet('Sahithi')
}Functions As Input Arguments
In plain words: You can pass one function as an input into another function (a callback), letting the outer function run your custom logic at the right moment.
In V, functions are first-class types. This means that a function signature can be used as a parameter type for another function, allowing you to pass functional logic as an argument (a pattern known as a higher-order function). The type syntax for a function parameter matches its signature, such as f fn () string, representing a function f that takes no parameters and returns a string. You can pass named functions or anonymous functions inline directly.
This example declares multiple greeting helpers and a higher-order function greet that takes a greeting function and a name to construct a message.
module main
fn greet_morning() string {
return 'Good Morning'
}
fn greet_noon() string {
return 'Good Afternoon'
}
fn greet_evening() string {
return 'Good Evening'
}
// 'greet' is a higher-order function accepting 'f' (a function returning a string)
fn greet(f fn () string, name string) string {
// Call the passed function 'f' dynamically and interpolate the result
return '${f()}, ${name}!'
}
fn main() {
// Pass a named function 'greet_morning' directly
mut res := greet(greet_morning, 'Pavan')
println(res)
// Pass another named function 'greet_evening'
res = greet(greet_evening, 'Sahithi')
println(res)
// Pass an anonymous function inline directly as the argument
res = greet(fn () string {
return 'New year greetings to you'
}, 'Sahithi')
println(res)
}Functions That Return Other Functions
In plain words: In V, a function can actually build and return another function as its result! This is called a higher-order function and is great for creating customizable helpers.
What happens in this example:
- Creates helper function(s)/method(s):
adder, subtractor, multiplier, fetch. - Uses
mutso the variable or collection is allowed to be updated after creation. - Prints the result to the terminal window so you can see the output right away.
module main
enum Operation {
add
sub
mul
}
fn adder(i int, j int) int {
return i + j
}
fn subtractor(i int, j int) int {
return i - j
}
fn multiplier(i int, j int) int {
return i * j
}
fn fetch(op Operation) fn (int, int) int {
return match op {
.add {
adder
}
.sub {
subtractor
}
.mul {
multiplier
}
}
}
fn main() {
i, j := 2, 5
mut f := fetch(.add) // return adder function
mut res := f(i, j) // calls adder(2, 5)
println('sum of ${i} and ${j}: ${res}')
f = fetch(.sub) // returns subtractor function
res = f(i, j) // calls subtractor(2, 5)
println('difference of ${i} and ${j}: ${res}')
f = fetch(.mul) // returns multipler function
res = f(i, j) // calls multiplier(2, 5)
println('product of ${i} and ${j}: ${res}')
}Lambda Expressions
In plain words: When passing a short helper function into methods like .filter(...) or .map(...), V lets you write compact inline functions effortlessly.
V supports Lambda Expressions, which are lightweight, inline anonymous functions defined using the |variables| expression syntax.
Key architectural characteristics:
- Scope Restriction: Lambda expressions are not general-purpose functions; this syntax is only valid when passed directly as arguments to higher-order functions like
.sort(),.map(), and.filter(). - Implicit Returns: The result of the single expression on the right-hand side is automatically returned. No
returnkeyword is needed.
Step-by-Step Code Walkthrough:
- Sorting (
nums.sort(|a, b| b < a)):
The .sort() method accepts a comparator callback. The lambda expression defines parameters a and b, returning the comparison b < a to sort the array in descending order.
- Mapping (
nums.map(|x| x * 10)):
The .map() method transforms each array element. The lambda |x| x * 10 accepts the element x, multiplies it by 10, and produces the new mapped array.
- Filtering (
doubled.filter(|x| x > 20)):
The .filter() method checks a predicate. The lambda |x| x > 20 checks each element and retains only those returning true.
module main
fn main() {
mut nums := [1, 3, 2, 5, 4]
// Sort descending using a lambda expression
nums.sort(|a, b| b < a)
println('Sorted: ${nums}') // [5, 4, 3, 2, 1]
// Map using lambda to multiply by 10
doubled := nums.map(|x| x * 10)
println('Doubled: ${doubled}') // [50, 40, 30, 20, 10]
// Filter using lambda to keep only elements > 20
filtered := doubled.filter(|x| x > 20)
println('Filtered (>20): ${filtered}') // [50, 40, 30]
}Closures
In plain words: A closure is an anonymous function that explicitly lists outer variables inside [var_name] so it can remember and use those variables inside its body.
V supports Closures, which are anonymous functions that "remember" and access variables from the parent scope in which they were created.
Unlike languages where variable capture is automatic and hidden, V implements explicit capture lists for safety and predictability:
- Explicit Capture Syntax: Captured variables must be declared inside square brackets
fn [captured_var] (args). - Pass-by-Value Capture (
[captured_var]): The variable's value is copied when the closure is created. It is read-only inside the closure body. - Pass-by-Reference Capture (
[mut captured_var]): Prepending the capture withmutpasses the variable by reference. Any changes made to the variable inside the closure modify the original variable in the parent scope, and vice-versa.
Step-by-Step Code Walkthrough:
new_counterFunction: Returns a closurefn () int.- State Preservation (
[mut count]): The closure captures the local variablecountfromnew_counterasmut. This allowscountto survive afternew_counterreturns and update its state across multiple invocation calls (e.g.counter()). - Value Capture (
[factor]): The closuremultipliercaptures thefactorvariable as read-only. Callingmultiplier(5)evaluates to5 * 10(50).
module main
fn new_counter() fn () int {
mut count := 0
// The closure inherits `count` by reference (read-write) using explicit list `[mut count]`
return fn [mut count] () int {
count++
return count
}
}
fn main() {
counter := new_counter()
println(counter()) // 1
println(counter()) // 2
println(counter()) // 3
// An immutable capture closure
factor := 10
multiplier := fn [factor] (x int) int {
return x * factor
}
println(multiplier(5)) // 50
}Chapter 7 Structs (Custom Types)
Plain-English Chapter Guide (Start Here if You Are New to Coding!)
- The Big Idea in Simple Words: Basic types like
stringandintonly hold one piece of information. What if you want to represent aUser(who has aname, anage, and anemail) or aCar(with abrandandspeed)? A struct lets you design your own custom "form" or "blueprint" that bundles related pieces of data (fields) together into one neat package. You can also attach actions (methods) directly to your struct! - Words to Know:
- Struct (
struct User { ... }): A custom blueprint that groups several named variables together. - Field (
user.name): One individual piece of data stored inside a struct. - Method (
fn (u User) greet()): A function attached to a struct so you can calluser.greet(). - Access Modifiers (
mut:,pub:,pub mut:): Labels inside a struct that control whether fields can be changed (mut) or seen by other folders/modules (pub). - Struct Embedding: Putting one struct inside another so it automatically shares its fields.
Quick Access โ Jump to any lesson in this chapter
Below is an index of all code examples in this chapter. You can use these links to jump directly to any specific code example:
Struct Basics & Fields
- Defining Struct
- Initialize Struct Example 1
- Initialize Struct Example 2
- Access Struct Fields
- Heap Structs
- Updating Immutable Struct Variable Throws Error
- Updating Mutable Fields Of Struct
- Updating Immutable Fields Throws Error
- Updating Struct With Unspecified Fields Are Zeroed
- Struct With Multiple Fields
- Grouping Struct Fields Based On Access Modifiers
- Required Fields Example 01
- Required Fields Example 02
- Struct Fields With Default Values
- Methods For Struct
- Mutable Methods
- Printing Custom Types
- Adding Struct As Struct Field
- Updating Fields Of Type Struct
- Struct As Trailing Literal Arguments To Function
- Anonymous Structs
- Static Type Methods
- noinit Structs
- Unions
Structs are user-defined data structures that allow you to group related fields together. This chapter explains how to define structs, set default values, make fields required, attach methods to structs, and embed structs inside other structures.
Under the Hood: Struct Alignment, Method Dispatch & Heap Allocation
๐ง Optional Deep Dive โ Click to Expand / Collapse
Plain-English Summary: In simple words: Structs pack their fields tightly together in fast memory (the stack) by default, and V knows at compile time exactly which struct method to call with zero lookup delay.
Structs are V's primary tool for domain modeling and data abstraction. V avoids object-oriented complexity (no classes, no inheritance) while providing powerful composition and method dispatch.
Memory Layout, Alignment & Padding
The V compiler lays out struct fields in sequential memory order, adhering to natural CPU word alignment rules (4 bytes on 32-bit, 8 bytes on 64-bit systems):
struct Example {
a i64 // 8 bytes (offset 0)
b i16 // 2 bytes (offset 8)
// 6 bytes of padding inserted here
c &u8 // 8 bytes (offset 16)
}Optimization Tip: Grouping larger fields (i64, pointers) before smaller fields (i16, bool) minimizes padding bytes inserted by the compiler, reducing overall struct memory footprint.
Method Receiver Dispatch Mechanics
Methods in V are defined with a receiver argument:
fn (u User) full_name() string { ... }
fn (mut u User) set_age(age int) { ... }Under the hood, V compiles methods into standard C functions where the receiver is passed as the first argument:
* User.full_name() compiles to string main__User_full_name(User u).
* User.set_age() compiles to void main__User_set_age(User* u, int age).
Because method dispatch is static, there is zero virtual table (vtable) lookup overheadโmethod calls execute as fast as direct C function invocations.
Struct Embedding (Composition vs Inheritance)
V does not support class inheritance. Instead, V uses struct embedding:
struct User {
name string
email string
}
struct Admin {
User // Embedded struct
role string
}All fields and methods of User are automatically promoted to Admin. Under the hood, Admin simply contains User as its first contiguous field, ensuring clean composition with zero indirection overhead.
The `@[heap]` Attribute
For structs that represent large buffers or deeply nested data structures, applying @[heap] instructs the compiler to enforce that instances are always created on the heap via references (&MyStruct{...}), preventing stack overflows.
Struct Basics & Fields
Defining Struct
In plain words: The struct keyword lets you invent a brand-new custom data type by listing the named fields (pieces of information) that belong together.
What happens in this example:
- Defines custom data blueprint(s):
Note, demo. - Prints the result to the terminal window so you can see the output right away.
struct Note {
id int
message string
}
fn main() {
note := Note{
id: 1
message: 'A simple struct demo'
}
println(note)
}Initialize Struct Example 1
In plain words: The struct keyword lets you invent a brand-new custom data type by listing the named fields (pieces of information) that belong together.
What happens in this example:
- Defines custom data blueprint(s):
Note, demo. - Prints the result to the terminal window so you can see the output right away.
struct Note {
id int
message string
}
fn main() {
note := Note{1, 'A simple struct demo'}
println('ID: ${note.id}')
println('Message: ${note.message}')
}Initialize Struct Example 2
In plain words: The struct keyword lets you invent a brand-new custom data type by listing the named fields (pieces of information) that belong together.
What happens in this example:
- Defines custom data blueprint(s):
Note, demo. - Prints the result to the terminal window so you can see the output right away.
struct Note {
id int
message string
}
fn main() {
note := Note{
message: 'A named-field struct demo'
id: 2
}
println(typeof(note).name)
println(note)
}Access Struct Fields
In plain words: Use a dot (.) after a struct variable's nameโlike n.message or n.idโto read the individual fields stored inside that struct.
What happens in this example:
- Defines custom data blueprint(s):
Note, demo. - Prints the result to the terminal window so you can see the output right away.
struct Note {
id int
message string
}
fn main() {
n := Note{1, 'a simple struct demo'}
println(n.message)
}Heap Structs
In plain words: Putting & in front of a struct (&Note{...}) places it in long-lived heap memory and gives you a lightweight reference (&Note) to it.
By default, V allocates struct instances on the stack, which is fast and manages memory automatically when variables go out of scope. However, for large structures or instances that must survive beyond the current function scope, you should allocate them on the heap. In V, you allocate a struct on the heap by prepending the initialization literal with the reference operator & (e.g., &MyStruct{}). The type of a heap-allocated struct is a pointer type, represented as &MyStruct (read-only reference).
This example demonstrates declaring a heap-allocated struct instance and printing its type name.
struct Note {
id int
message string
}
fn main() {
// Prepending & allocates the Note instance on the heap rather than the stack.
// 'n1' holds a reference (pointer) to the heap-allocated note.
n1 := &Note{1, 'this note will be allocated on heap'}
// Prints the type of n1, which is &Note (pointer/reference type)
println(typeof(n1).name) // &Note
}Updating Immutable Struct Variable Throws Error
In plain words: Even if a struct has changeable (mut:) fields inside its blueprint, you still cannot change them unless the struct variable itself was created with mut!
What happens in this example:
- Defines custom data blueprint(s):
Note, demo, updated. - Prints the result to the terminal window so you can see the output right away.
module main
struct Note {
id int
mut:
message string
}
fn main() {
n := Note{1, 'a simple struct demo'}
println(n)
n.message = 'a simple struct updated' // throws error
}Updating Mutable Fields Of Struct
In plain words: When both the struct field is under a mut: section AND your struct variable is declared with mut, you can freely update my_struct.field = new_value.
In V, struct fields are read-only (immutable) by default. To make specific fields mutable, you must group them under the mut: access modifier within the struct definition. However, defining a field as mutable only makes it eligible for mutation; to actually modify the field on a struct instance at runtime, the instance itself must be declared as a mutable variable using the mut keyword (e.g., mut my_instance := MyStruct{}). If the instance is declared as immutable, the compiler will reject any attempt to modify its fields even if they are defined under mut:.
This example defines a Note struct with a mutable message field, initializes a mutable instance, and updates its value.
module main
struct Note {
// 'id' is immutable by default and cannot be updated.
id int
mut:
// 'message' is declared mutable, allowing updates if the struct instance is mutable.
message string
}
fn main() {
// Declare the struct instance 'n' as mutable using the 'mut' keyword
mut n := Note{1, 'a simple struct demo'}
println('before update')
println(n)
// Modify the mutable field message
n.message = 'a simple struct updated'
println('after update')
println(n)
}Updating Immutable Fields Throws Error
In plain words: By default, fields listed at the top of a struct are locked (immutable). Trying to change a field that isn't under a mut: section inside the struct definition will be stopped by the compiler.
What happens in this example:
- Defines custom data blueprint(s):
Note, demo. - Uses
mutso the variable or collection is allowed to be updated after creation.
module main
struct Note {
id int
mut:
message string
}
fn main() {
mut j := Note{1, 'a simple struct demo'}
j.id = 2 // throws error
}Updating Struct With Unspecified Fields Are Zeroed
In plain words: Whenever you create a struct and leave out some fields, V automatically fills those omitted fields with safe zero defaults (0 for numbers, '' for strings, false for booleans)โnever random garbage!
What happens in this example:
- Code note: unspecified fields zeroed by default
- Code note: id being type of int, will become 0 here
module main
struct Note {
id int
mut:
message string
}
fn main() {
// declare
mut n := Note{}
// populate
n = Note{
id: 1
message: 'updating struct fields demo'
}
println(n)
// unspecified fields zeroed by default
// id being type of int, will become 0 here
println('unspecified id zeroed during short struct type initialization')
n = Note{
message: 'updating struct fields demo 2'
}
println(n)
}Struct Update Syntax
In plain words: Writing User{ ...old_user, age: 31 } creates a new User that copies every field from old_user except the specific fields you override!
V provides a convenient syntax to create and return a modified copy of a struct instance by spreading the original fields using ... and overwriting specific ones:
What happens in this example:
- Code note: Returns a modified copy using the struct update syntax
module main
struct User {
name string
age int
is_registered bool
}
fn register(u User) User {
// Returns a modified copy using the struct update syntax
return User{
...u
is_registered: true
}
}
fn main() {
println('=== Struct Update Syntax ===')
user1 := User{
name: 'Ada'
age: 36
}
user2 := register(user1)
println('user1: ${user1}') // User{name: 'Ada', age: 36, is_registered: false}
println('user2: ${user2}') // User{name: 'Ada', age: 36, is_registered: true}
assert user1.is_registered == false
assert user2.is_registered == true
assert user2.name == 'Ada'
assert user2.age == 36
}Struct With Multiple Fields
In plain words: A struct can group together as many fields of different types (int, string, bool, arrays, etc.) as your data model needs.
What happens in this example:
- Defines custom data blueprint(s):
Note.
struct Note {
id int
mut:
message string
status bool
}
fn main() {
}Grouping Struct Fields Based On Access Modifiers
In plain words: Inside a struct, you organize fields under section headers (mut:, pub:, pub mut:) to control which fields can be changed or seen from outside the module.
What happens in this example:
- Defines custom data blueprint(s):
Note.
pub struct Note {
pub:
id int
pub mut:
message string
status bool
}
fn main() {
}Required Fields Example 01
In plain words: Adding @[required] after a struct field tells V: 'Never let anyone create this struct if they forget to provide a value for this field!'
What happens in this example:
- Code note: throws error
pub struct Note {
pub:
id int
pub mut:
message string @[required]
status bool
}
fn main() {
_ := Note{
id: 1
status: false
}
}
// throws errorRequired Fields Example 02
In plain words: Adding @[required] after a struct field tells V: 'Never let anyone create this struct if they forget to provide a value for this field!'
What happens in this example:
- Defines custom data blueprint(s):
Note, demo. - Prints the result to the terminal window so you can see the output right away.
module main
pub struct Note {
pub:
id int
pub mut:
message string @[required]
status bool
}
fn main() {
n := Note{
id: 1
message: 'a simple struct demo'
status: false
}
println(n)
}Struct Fields With Default Values
In plain words: You can give any struct field a custom starting value (like status string = 'active') right inside the struct definition so callers don't have to type it every time.
What happens in this example:
- Defines custom data blueprint(s):
Note. - Prints the result to the terminal window so you can see the output right away.
import time
pub struct Note {
pub:
id int
created time.Time = time.now()
pub mut:
message string @[required]
status bool
due time.Time = time.now().add_days(1)
}
fn main() {
n := Note{
id: 1
message: 'order groceries'
}
println(n)
}Methods For Struct
In plain words: Putting (s StructName) right after fn attaches that function to your struct as a method, letting you call my_struct.method_name().
What happens in this example:
- Code note: 1. Define a Struct.
- Code note: Structs are defined with the
structkeyword. By default, structs are private - Code note: (only accessible within the current module) and all fields are immutable.
- Code note: The
pubkeyword makes the struct visible to other modules.
module main
import time
// 1. Define a Struct.
// Structs are defined with the `struct` keyword. By default, structs are private
// (only accessible within the current module) and all fields are immutable.
// The `pub` keyword makes the struct visible to other modules.
pub struct Note {
// 2. Struct Access Modifiers:
// - `pub:` makes the fields readable from outside the module, but still immutable.
pub:
id int
// Fields can have default values assigned at declaration.
created time.Time = time.now()
// - `pub mut:` makes the fields readable and writable from outside the module.
pub mut:
// Attributes can be attached to struct fields.
// `@[required]` specifies that this field must be explicitly provided when instantiating.
message string @[required]
status bool
due time.Time = time.now().add_days(1)
}
// 3. Define a Method (Value Receiver).
// In V, a method is a function with a receiver argument.
// The receiver is specified in parentheses before the function name: `(n Note)`.
// This is a "value receiver" method, meaning it receives a copy of the struct instance.
// It cannot modify fields on the original struct instance.
pub fn (n Note) is_empty_message() bool {
return n.message.len < 1
}
fn main() {
// 4. Instantiate a Struct.
// We use the struct name and curly braces, listing field initializations.
// Because `message` is marked `@[required]`, we must specify it.
// The variable `n` is marked `mut` because we might want to update its `pub mut` fields.
mut n := Note{
id: 1
message: ''
}
// 5. Invoke Struct Methods.
// Methods are called on struct instances using the dot operator.
if n.is_empty_message() {
println('message is empty')
} else {
println('message not empty')
}
}Mutable Methods
In plain words: If a struct method needs to modify the struct's own fields, mark the receiver with mut: fn (mut u User) birthday() { u.age++ }.
By default, struct methods in V receive a read-only copy of the struct instance (value receiver). If a method needs to modify any fields of the struct, it must declare a mutable receiver using the mut keyword, e.g., fn (mut n Note) mark_as_completed().
Additionally, the struct instance variable must be declared with mut at the call site to allow mutable method invocations.
module main
import time
pub struct Note {
pub:
id int
created time.Time = time.now()
pub mut:
message string @[required]
status bool
due time.Time = time.now().add_days(1)
}
// 1. Define a Mutable Struct Method.
// To modify fields of a struct inside a method, the receiver must be marked mutable: `(mut n Note)`.
// Under the hood, this passes a pointer/mutable reference, allowing the method to update
// the original struct instance fields directly.
pub fn (mut n Note) mark_as_completed() {
n.status = true
println('Note [ID: ${n.id}] marked as completed.')
}
// 2. Define another Mutable Method to update the message.
pub fn (mut n Note) update_message(new_msg string) {
if new_msg.len > 0 {
n.message = new_msg
println('Note [ID: ${n.id}] message updated to: "${new_msg}"')
}
}
fn main() {
// 3. Instantiate a mutable struct instance.
// To call mutable methods on a struct, the variable MUST be declared as mutable (`mut`).
// If `n` was immutable, calling `n.mark_as_completed()` would result in a compilation error.
mut n := Note{
id: 42
message: 'Learn V programming'
status: false
}
println('Initial state - Message: "${n.message}", Completed: ${n.status}')
// 4. Call the mutable methods.
n.update_message('Master V programming and C interop!')
n.mark_as_completed()
// 5. Verify the updates.
println('Final state - Message: "${n.message}", Completed: ${n.status}')
assert n.status == true
}Printing Custom Types
In plain words: Define a pub fn (x MyStruct) str() string method on any struct to customize how it looks when printed with println(x) or ${x}!
By default, passing a struct instance to functions like println will print its fields in a default format (e.g., Color{r: 255, g: 0, b: 0}). However, if you want a custom, human-readable string representation of your struct type, you can define a str() string method on it. V's runtime automatically checks for this method when converting values to strings or when outputting using printing functions.
This example illustrates defining a str() string method on a custom Color struct to format it as {r, g, b}.
module main
struct Color {
r int
g int
b int
}
// By defining a method named str() returning a string, we customize how Color is printed.
pub fn (c Color) str() string {
return '{${c.r}, ${c.g}, ${c.b}}'
}
fn main() {
red := Color{
r: 255
g: 0
b: 0
}
// println automatically detects and calls the custom .str() method on Color
println(red)
}Adding Struct As Struct Field
In plain words: A struct can hold another struct as one of its fields (for example, an Author struct inside a Book struct) so you can model rich, nested real-world data.
What happens in this example:
- Code note: NoteTimeInfo is a struct to store time info of Note
- Code note: Note is a struct with struct NoteTimeInfo as a field, along with other fields
import time
// NoteTimeInfo is a struct to store time info of Note
pub struct NoteTimeInfo {
pub:
created time.Time = time.now()
pub mut:
due time.Time = time.now().add_days(1)
}
// Note is a struct with struct NoteTimeInfo as a field, along with other fields
pub struct Note {
NoteTimeInfo // Struct as another struct field
pub:
id int
pub mut:
message string @[required]
status bool
}
fn main() {
n := Note{
id: 1
message: 'adding struct as struct field demo'
}
println('Due date: ${n.due}')
println(n)
}Updating Fields Of Type Struct
In plain words: When a struct contains another struct inside it, you can chain dots (book.author.name = '...') to read or update the inner struct's fields (as long as both are marked mut).
What happens in this example:
- Code note: NoteTimeInfo is a struct to store time info of Note
- Code note: Note is a struct with struct NoteTimeInfo as a field, along with other fields
- Code note: approach 1: implicit access of struct fields of fields of type struct
- Code note: approach 2: explicitly specifying the field of type struct and its fields
module main
import time
// NoteTimeInfo is a struct to store time info of Note
pub struct NoteTimeInfo {
pub:
created time.Time = time.now()
pub mut:
due time.Time = time.now().add_days(1)
}
// Note is a struct with struct NoteTimeInfo as a field, along with other fields
pub struct Note {
NoteTimeInfo
pub:
id int
pub mut:
message string @[required]
status bool
}
fn main() {
mut n := Note{
id: 1
message: 'adding struct as struct field demo'
}
println('Due date: ${n.due}')
// approach 1: implicit access of struct fields of fields of type struct
n.due = n.due.add_days(2)
println('Due date after update: ${n.due}')
// approach 2: explicitly specifying the field of type struct and its fields
n.NoteTimeInfo.due = n.NoteTimeInfo.due.add_days(2)
println('Due date updated second time: ${n.due}')
println(n)
}Struct As Trailing Literal Arguments To Function
In plain words: When a function takes a config struct as its last parameter, V lets you pass the struct's field: value pairs directly inside the function call my_func(id: 1, title: 'Hi') without even writing the struct's name and curly braces!
What happens in this example:
- Code note: NoteTimeInfo is a struct to store time info of Note
- Code note: Note is a struct with embedding struct NoteTimeInfo along with other fields
module main
import time
// NoteTimeInfo is a struct to store time info of Note
pub struct NoteTimeInfo {
pub:
created time.Time = time.now()
pub mut:
due time.Time = time.now().add_days(1)
}
// Note is a struct with embedding struct NoteTimeInfo along with other fields
pub struct Note {
NoteTimeInfo
pub:
id int
pub mut:
message string @[required]
status bool
}
fn new_grocery_note(n Note) &Note {
return &Note{
id: n.id
message: 'Buy Groceries: ' + n.message
}
}
fn extend_due_by_a_day(n Note) &Note {
return &Note{
NoteTimeInfo: NoteTimeInfo{
due: n.due.add_days(1)
}
id: n.id
message: n.message
}
}
fn main() {
g := new_grocery_note(Note{ id: 1, message: 'Milk' })
println('${g.message} is due by ${g.due}')
n := extend_due_by_a_day(g)
println('After extending due date by a day')
println('${n.message} is due by ${n.due}')
}Trailing Struct Literal Arguments
In plain words: When a function's parameter is a struct, V lets you pass field: value arguments directly inside my_fn(field: value) for super clean configuration calls.
V does not support default function arguments or named arguments. Instead, you can use trailing struct literal arguments. By tagging a configuration struct with the @[params] attribute, V allows you to omit both the struct name and the curly braces when calling the function if it is the final argument.
module main
@[params]
struct ButtonConfig {
text string
is_disabled bool
width int = 70
height int = 20
}
struct Button {
text string
width int
height int
}
fn new_button(c ButtonConfig) &Button {
return &Button{
width: c.width
height: c.height
text: c.text
}
}
fn main() {
println('=== Trailing Struct Literal Arguments ===')
// Omitting both the struct name and braces
button := new_button(text: 'Click me', width: 100)
println('button: width=${button.width}, height=${button.height}, text="${button.text}"')
assert button.height == 20
assert button.width == 100
assert button.text == 'Click me'
}Anonymous Structs
In plain words: For one-off nested data that doesn't need a reusable struct name across your whole file, you can define an inline struct { ... } field directly.
V supports Anonymous Structs which are inline struct declarations without separate struct names. They are useful for one-off structures like local nested objects.
Step-by-Step Code Walkthrough:
- Inline Sub-Struct Declaration:
In the Book struct definition, the author field is declared as an anonymous struct type with fields name string and age int. No named struct like Author is required.
- Inline Struct Initialization:
Inside main(), we instantiate Book. The nested author field is initialized directly using struct { name: 'Samantha Black', age: 24 }, matching the field structure.
- Field Access:
Nested fields are accessed sequentially using dot notation: book.author.name and book.author.age.
module main
import json2
struct Book {
title string
mut:
author struct {
name string
mut:
age int
}
}
fn main() {
mut book := Book{
title: 'The V Programming Language'
author: struct {
name: 'Samantha Black'
age: 24
}
}
book.author.age = 25
println('${book.title} by ${book.author.name} (${book.author.age})')
println(json2.encode(book))
}Static Type Methods
In plain words: Writing fn TypeName.new(...) defines a static factory function you call on the type name itself (User.new()), rather than on an existing instance.
V supports Static Type Methods (e.g. User.new()). These are defined on a struct via fn [StructName].[methodName] and allow organizing all constructor/factory functions related to a struct. V does not have traditional class constructors; static type methods are standard functions namespace-associated with the struct.
Step-by-Step Code Walkthrough:
- Static Method Definition:
fn User.new(name string, age int) User declares a static method named new associated with the User struct namespace. It returns a new User instance.
- Factory Organization:
The static method User.default_user() calls User.new('Guest', 18) to construct a user with default values, acting as a clean factory builder.
- Invocation Syntax:
Inside main(), static methods are invoked using the struct name prefix: User.new(...) and User.default_user(). This prevents global namespace pollution and groups constructor-like logic cleanly.
module main
struct User {
name string
age int
}
// Defining a static type method on User
fn User.new(name string, age int) User {
return User{
name: name
age: age
}
}
// Another static method
fn User.default_user() User {
return User.new('Guest', 18)
}
fn main() {
// Call static type methods using StructName.method_name()
user1 := User.new('Bob', 25)
user2 := User.default_user()
println('User 1: ${user1.name}, Age: ${user1.age}')
println('User 2: ${user2.name}, Age: ${user2.age}')
}noinit Structs
In plain words: Tagging a struct with @[noinit] stops outside modules from creating the struct directly with MyStruct{}, forcing them to use your safe new() constructor function.
V supports [noinit] structs which are structs that cannot be initialized directly outside of their declaring module. This forces client code to use factory constructor functions to instantiate the struct, enabling strict initialization checks and API boundaries.
Step-by-Step Code Walkthrough:
- Declaring [noinit]:
In the noinit_config module (noinit_config.v), the Config struct is marked with the @[noinit] attribute. This blocks external modules from directly initializing it using literals like noinit_config.Config{ ... }.
- Exposing a Constructor:
We provide a public factory function pub fn new_config(port int, host string) Config inside the noinit_config module, which is authorized to initialize and return the struct.
- Compiler Enforcement:
In the main module (noinit_structs.v), creating noinit_config.new_config(...) compiles and runs successfully. Attempting to directly write noinit_config.Config{port: 8080} would cause a compilation error.
module noinit_config
@[noinit]
pub struct Config {
pub:
port int
host string
}
// Public constructor function to allow initialization from outside
pub fn new_config(port int, host string) Config {
return Config{
port: port
host: host
}
}import noinit_config
fn main() {
// This works because it uses the constructor function
cfg := noinit_config.new_config(8080, 'localhost')
println('Config port: ${cfg.port}, host: ${cfg.host}')
// This would fail compilation because noinit_config.Config is marked [noinit]:
// cfg2 := noinit_config.Config{ port: 8080, host: 'localhost' }
}Unions
In plain words: A union shares the same memory spot for multiple fields (only one field is active at a time). Used mostly when talking to low-level C libraries.
A Union is a special type of struct that allows storing different data types in the same memory location. The largest member defines the size of the union. All members share the same memory location, meaning modifying one member automatically modifies the shared representation of the others. Union field access is considered memory-unsafe and must always be performed inside unsafe {} blocks.
Step-by-Step Code Walkthrough:
- Union Declaration & Mutability:
union Data declares two fields: f f64 (8 bytes) and i int (4 bytes). Because they are in a union, they share the same starting memory address, and the total size of Data is 8 bytes. By default in V, union fields are immutable; we must place them under a mut: block inside the union declaration to allow their values to be reassigned.
- Memory Corruption Demonstration:
We initialize the union with an integer i: 10.
Inside unsafe { ... }, when we assign d.f = 5.5, the float value overwrites the shared memory. Reading d.i subsequently returns a garbled integer representing the binary layout of the float 5.5, demonstrating the shared storage layout.
- Safety Restriction:
Accessing any field of a union (d.i or d.f) is blocked by the compiler unless wrapped in an unsafe block, protecting developers from accidental memory misinterpretation.
module main
// Define a union sharing the same memory location, marked mutable
union Data {
mut:
f f64
i int
}
fn main() {
mut d := Data{
i: 10
}
// Accessing union members must be performed in an unsafe block
unsafe {
println('Union int value: ${d.i}')
// Modifying one member automatically modifies the other since they share memory
d.f = 5.5
println('Union float value: ${d.f}')
println('Union int value after float update: ${d.i} (shared memory representation)')
}
}Structs with Reference Fields
In plain words: A struct can hold a pointer/reference (&OtherStruct) to another struct so multiple objects can point to the same shared item.
Structs can store reference fields/pointers (prefixed with &). Reference fields must be initialized to a valid address or explicitly auto-initialized using = unsafe { nil } (use with caution, as nil pointer dereferences will crash/panic).
module main
struct Node {
a &Node
b &Node = unsafe { nil } // Auto-initialized to nil
}
fn main() {
println('=== Struct Reference Fields ===')
foo := Node{
a: unsafe { nil }
}
bar := Node{
a: &foo
}
println('foo: ${foo}')
println('bar: ${bar}')
assert bar.a == &foo
}Chapter 8 Error Handling
Plain-English Chapter Guide (Start Here if You Are New to Coding!)
- The Big Idea in Simple Words: In real life, things don't always go as planned: a file might be missing, a user might type letters instead of a number, or you might search for a username that doesn't exist. Many programming languages crash with scary "Null Pointer" errors when this happens. V protects you by making "might be empty" (
?Type, called an Option) and "might fail with an error" (!Type, called a Result) super clear, and gives you the simpleor { ... }safety net to handle problems calmly. - Words to Know:
- Option (
?string): A function return type meaning: "I will give you astring, OR I might returnnone(nothing found)." - Result (
!int): A function return type meaning: "I will give you anint, OR I might return anerror('reason')if something goes wrong." or { ... }Block: A safety net placed right after a function call that runs if the function returnednoneor an error.- Propagation (
!or?at the end of a call): Passes an error ornoneup to the calling function so it can handle it there.
Quick Access โ Jump to any lesson in this chapter
Below is an index of all code examples in this chapter. You can use these links to jump directly to any specific code example:
Option & Result Types
V has no exceptions. Instead, it handles errors using Option and Result types, which are checked at compile time. This chapter teaches you how to write robust, error-free programs using V's clean error handling syntax.
Under the Hood: Option (`?T`) and Result (`!T`) Representation
๐ง Optional Deep Dive โ Click to Expand / Collapse
Plain-English Summary: In simple words: Instead of using slow crash-recovery exceptions like other languages, V packages 'success or error' into a tiny, fast status flag that you handle right on the spot with or { ... }.
V completely eliminates traditional exception handling (try / catch / throw). Instead, error handling is explicit, statically typed, and verified at compile time.
Zero-Exception Overhead
In languages with exceptions (C++, Java, Python), throwing an exception triggers expensive stack unwinding, symbol resolution, and runtime handler table traversal.
In V, ?T (Option) and !T (Result) types are implemented as compact C tagged unions:
struct _Result_int {
bool is_error;
union {
int data;
IError* err;
};
};When a function succeeds, returning a value incurs zero heap allocationโthe value is passed directly in registers or stack memory with is_error = false.
Option (`?T`) vs Result (`!T`)
| Type | Syntax | Purpose | Unwrapped Failure State |
|---|---|---|---|
| Option Type | ?T | Represents presence or absence of a value (e.g. searching a key, cache lookup). | none (missing value without an error message) |
| Result Type | !T | Represents success or operational failure (e.g. file I/O, network requests, database queries). | IError (custom error struct or error message) |
The `or { ... }` Block Compilation
Calling a function that returns !T or ?T mandates handling the failure case with an or { ... } block:
content := os.read_file('config.json') or {
eprintln('Failed to read config: ${err}')
return
}Under the hood:
- The compiler checks the
is_errordiscriminant tag. - If successful, the payload is unwrapped directly into
content. - If an error occurred, the compiler exposes the special
errvariable inside theorblock scope and statically requires control flow to diverge (usingreturn,panic(),exit(),break,continue, or evaluating to a fallback default value).
Error Propagation (`!`)
To propagate an error upward to the calling function without handling it locally, append !:
fn load_user_data(path string) !UserData {
raw := os.read_file(path)! // Propagates error immediately if read_file fails
return json2.decode[UserData](raw)!
}Option & Result Types
Error Handling
In plain words: Walks through V's complete safety-net system: ?Type (Options for values that might be none), !Type (Results for operations that might return an error), or { ... } handlers, and custom IError types.
In many programming languages, errors are handled using exceptions (with try, catch, and throw blocks). Exception blocks can make code hard to read and trace because control flow can jump unpredictably.
V takes a different approach. V does not have exceptions. Instead, V handles errors explicitly using two main concepts:
- Option Types (
?T): Used when a value might simply be missing (like searching for an item that isn't in a list). - Result Types (
!T): Used when an operation might actually fail with a specific error (like division by zero or a database timeout).
By forcing you to handle these outcomes explicitly, V makes your code safer and easier to debug.
module main
// ==========================================
// Define Custom Error Types
// ==========================================
// CustomError embeds the builtin Error struct to implement the IError interface.
struct CustomError {
Error // Required: provides default implementations of msg() and code()
message string
code int
}
// Overwrite the msg() method for CustomError
fn (err CustomError) msg() string {
return err.message
}
// Overwrite the code() method for CustomError
fn (err CustomError) code() int {
return err.code
}
// DatabaseError represents another custom error type.
struct DatabaseError {
Error
query string
}
fn (err DatabaseError) msg() string {
return 'Database error executing query: "${err.query}"'
}
// ==========================================
// 1. Option Types (?T)
// Options represent either a value of type T or nothing (none).
// ==========================================
// find_item returns a string if found, or none if not.
fn find_item(id int) ?string {
if id == 42 {
return 'V programming book'
}
return none // return absence of value
}
// find_item_wrapper demonstrates Option propagation with the `?` suffix.
fn find_item_wrapper(id int) ?string {
// If find_item returns none, the execution stops here and propagates none up.
item := find_item(id)?
return 'Found: ' + item
}
// ==========================================
// 2. Result Types (!T)
// Results represent either a value of type T or an IError.
// ==========================================
// divide performs float division but returns an error for division by zero.
fn divide(a f64, b f64) !f64 {
if b == 0.0 {
return error('division by zero') // Return a standard error
}
return a / b
}
// fetch_data returns a string or a CustomError.
fn fetch_data(success bool) !string {
if !success {
return CustomError{
message: 'Connection timed out'
code: 504
}
}
return 'Raw database records'
}
// query_db returns a string or a DatabaseError.
fn query_db(query string, success bool) !string {
if !success {
return DatabaseError{
query: query
}
}
return 'Query success'
}
// calculate_and_format demonstrates Result propagation using the `!` operator.
fn calculate_and_format(a f64, b f64) !string {
// The `!` suffix propagates the error to the caller if divide fails.
res := divide(a, b)!
return 'Result is ${res:.2f}'
}
// ==========================================
// 3. Unrecoverable Errors (Panics)
// ==========================================
fn force_panic() {
println('Simulating a critical failure...')
panic('Fatal error: Out of memory or system crash.')
}
fn main() {
println('=== 1. Option Types (?T) ===')
// Option Handling: Option unwrapping using `or` block
item_1 := find_item(42) or { 'Default Item' }
println('Item 1 (with 42): ${item_1}')
item_2 := find_item(99) or { 'Default Item' }
println('Item 2 (with 99): ${item_2}')
// Option Handling: Option unwrapping with variable binding using `if-let`
if item := find_item(42) {
println('If-let match: Found "${item}"')
} else {
println('If-let match: Item not found')
}
if item := find_item(99) {
println('If-let match: Found "${item}"')
} else {
println('If-let match: Item not found (none)')
}
// Option Propagation Check
wrapped_item := find_item_wrapper(99) or { 'None propagated successfully' }
println('Propagation check: ${wrapped_item}\n')
println('=== 2. Result Types (!T) ===')
// Result Handling: Standard error message extraction via the `err` variable inside `or` block
calc_success := calculate_and_format(10.0, 2.0) or { 'Error: ${err}' }
println('Calc success: ${calc_success}')
calc_fail := calculate_and_format(10.0, 0.0) or { 'Error: ${err}' }
println('Calc failure: ${calc_fail}')
println('\n=== 3. Custom Error Matching & Type Casting ===')
// We can inspect the error type dynamically using the `is` check inside the `or` block.
// Since fetch_data(false) returns a Result type (!string), the `or` block must either:
// 1. Terminate control flow (e.g. using return, panic, exit)
// 2. Evaluate to a fallback string value.
// We use `''` (empty string) here as the fallback value to satisfy this type requirement.
fetch_data(false) or {
if err is CustomError {
// Inside this block, `err` is smart-cast to CustomError automatically,
// allowing direct access to custom fields like `code`.
println('Caught CustomError! Message: "${err.msg()}", Code: ${err.code}')
} else {
println('Caught generic error: ${err.msg()}')
}
'' // Fallback empty string returned to satisfy the !string return type of the or block
}
// Similarly, query_db returns !string, so its or block must also evaluate to a string.
query_db('SELECT * FROM users', false) or {
if err is DatabaseError {
// Smart-cast to DatabaseError, accessing the `query` field
println('Caught DatabaseError!')
println('Query attempted: "${err.query}"')
println('Error message: "${err.msg()}"')
} else {
println('Caught generic error: ${err.msg()}')
}
'' // Fallback empty string returned to satisfy the !string return type of the or block
}
println('\n=== 4. Panic (Unrecoverable Error) ===')
// We wrap panic execution or run it last since it terminates the process.
// You can uncomment the line below to test panic termination:
// force_panic()
println('To run a panic, uncomment force_panic() in main.')
}Deep Dive Explanation
1. Option (`?T`) vs. Result (`!T`) Types
V enforces safety by separating missing data from actual runtime failures at the type system level:
- Option Type (
?T): Declares that a variable or function return value can either hold a value of typeTornone(denoting absence). Use options for operations like lookups or querying optional attributes. - Result Type (
!T): Declares that an operation returns either a value of typeTor an error that implements theIErrorinterface. Use results for operations that can fail due to external factors (e.g., IO, math division, DB connection).
2. Unwrapping with the `or` Block
When invoking a function that returns an Option or a Result, V requires you to explicitly unwrap it using an or block:
value := maybe_value() or { fallback_value }The or block acts as a recovery scope and must adhere to one of the following two rules:
- Provide a Fallback Value: It must evaluate to an expression matching type
T. - Halt or Divert Control Flow: It must use keywords like
return,panic(),exit(),break, orcontinueto exit the current scope.
For functions returning a Result type, V automatically exposes an implicit variable named err (of type IError) inside the or block. You can call err.msg() or err.code() to inspect the failure:
result := divide(10.0, 0.0) or {
println('Math error: ' + err.msg())
0.0
}3. Error and Option Propagation
Instead of handling errors immediately with an or block, you can bubble them up to the caller using propagation suffixes:
- Use the
?suffix to propagatenonefrom an optional-returning function:
item := find_item(id)? // Returns none to the caller if find_item fails- Use the
!suffix to propagate errors from a result-returning function:
res := divide(a, b)! // Propagates the IError up to the caller if b == 0.0 Note: A function can only use the propagation suffix if its own return type matches (i.e., returns ?U or !U respectively).
4. Custom Error Structs and the `IError` Interface
To build custom error types, define a struct and embed the builtin Error struct. Embedding Error ensures your custom struct implements the IError interface:
struct CustomError {
Error // Embed standard Error fields and methods
message string
code int
}You can override the msg() and code() methods to define how the error is printed and what status code it carries.
5. Type Assertions and Smart Casting with `is`
When handling generic IError values inside an or block, you can query their concrete types using the is keyword:
fetch_data(false) or {
if err is CustomError {
// V smart-casts 'err' to CustomError here
println('Custom code: ${err.code}')
}
''
}If the type check matches, V automatically smart-casts err inside that block, allowing you to access custom fields (like code or query) without explicit casting.
Chapter 9 Organizing Code with Modules
Plain-English Chapter Guide (Start Here if You Are New to Coding!)
- The Big Idea in Simple Words: When your project grows beyond one file, keeping all your code in a single giant file is like throwing all your clothes, dishes, and tools into one big box. Modules are simply folders that organize your code by topic (for example, a
mathfolder, adatabasefolder, or auifolder). You useimportto bring a module's tools into your file, andpubto mark which functions or structs are shared outside their folder. - Words to Know:
- Module (
module mymod): A folder of.vfiles that work together under one name. - Import (
import os): Loads a module so you can use its public functions (likeos.read_file(...)). - Public (
pub): Makes a function, struct, or constant visible to other modules outside its own folder. Withoutpub, it is private to its own folder. v.mod&vpm:v.modis your project's ID card;v installdownloads community packages from V's package manager (VPM).
Quick Access โ Jump to any lesson in this chapter
Below is an index of all code examples in this chapter. You can use these links to jump directly to any specific code example:
Modules & Project Structure
- Creating a Simple V Project - Main (modulebasics.v)
- Creating a Module - Helper (file1.v)
- Creating a Module - Main (modulebasics.v)
- Importing a Module - Helper (file1.v)
- Importing a Module - Main (modulebasics.v)
- Accessing Module Members - Helper (file1.v)
- Accessing Module Members - Main (modulebasics.v)
- Multiple Files (After Refactoring) - Helper 1 (file1.v)
- Multiple Files (After Refactoring) - Helper 2 (file2.v)
- Multiple Files (After Refactoring) - Main (modulebasics.v)
- Multiple Files (Before Refactoring) - Helper 1 (file1.v)
- Multiple Files (Before Refactoring) - Helper 2 (file2.v)
- Multiple Files (Before Refactoring) - Main (modulebasics.v)
- Member Scope (After Refactoring) - Helper 1 (file1.v)
- Member Scope (After Refactoring) - Helper 2 (file2.v)
- Member Scope (After Refactoring) - Main (modulebasics.v)
- Member Scope (Before Refactoring) - Helper 1 (file1.v)
- Member Scope (Before Refactoring) - Helper 2 (file2.v)
- Member Scope (Before Refactoring) - Main (modulebasics.v)
- Cyclic Imports - Module 1 Helper (file1.v)
- Cyclic Imports - Module 2 Helper (file1.v)
- Cyclic Imports - Main (modulebasics.v)
- Module Init & Cleanup Functions - Config (config.v)
- Module Init & Cleanup Functions - Helper (file1.v)
- Module Init & Cleanup Functions - Main (modulebasics.v)
- Accessing Module Constants - Helper (file1.v)
- Accessing Module Constants - Main (modulebasics.v)
- Accessing Module Structs - Helper (file1.v)
- Accessing Module Structs - Main (modulebasics.v)
Installing External Packages
Modules help organize larger codebases. In this chapter, you will learn how to create modules, import them, manage member visibility using pub, and understand module initialization lifecycle.
Under the Hood: Module Resolution, Namespaces & VPM
๐ง Optional Deep Dive โ Click to Expand / Collapse
Plain-English Summary: In simple words: In V, every folder is a module. Files in the same folder automatically share code, while other folders can only use items you explicitly mark with pub.
V uses a clean, folder-based module system that eliminates header files, forward declarations, and complex build scripts.
Single Module Per Directory Rule
In V:
* Every .v file located inside a single directory belongs to the same module namespace.
* All functions, structs, and constants defined across different .v files in that same directory are automatically visible to each other without importing or header declarations.
* This enables splitting large modules across multiple concise files cleanly.
src/
โโโ auth/
โ โโโ auth.v (module auth)
โ โโโ token.v (module auth - shares private symbols with auth.v)
โ โโโ user.v (module auth - shares private symbols with auth.v)
โโโ main.v (module main - imports auth)Public (`pub`) vs Module-Private Visibility
* By default, every function, struct, field, and constant in a module is module-private.
* Prepending pub (e.g. pub fn hash_password(...)) exports the symbol, making it accessible to external modules that import it.
* Struct fields follow granular visibility modifiers: pub:, mut:, pub mut:, or __global:.
Module Resolution Hierarchy
When an import mymodule statement is compiled, V searches for the module in the following order:
- Local project subdirectories relative to the importing file.
- Standard library modules located in the V installation directory (
vlib/). - Third-party modules installed in the global V module cache (
~/.vmodules/).
The `v.mod` Manifest & VPM (V Package Manager)
Every published module or reusable project includes a v.mod file at its root:
Module {
name: 'myproject'
description: 'High performance data pipeline'
version: '1.0.0'
license: 'MIT'
dependencies: ['net', 'sqlite']
}You can install packages from the VPM ecosystem with v install <package_name> or remove them with v remove <package_name>.
Modules & Project Structure
Creating a Simple V Project - Main (modulebasics.v)
In plain words: Every standalone V application starts with a module main file containing a fn main() entry point.
Think of a module as a small toolbox. The main module is the entry point of your program, while other modules can hold reusable functions and types.
This first example is intentionally simple: it shows the structure of a single-file V program before we introduce imports and shared modules.
module main
fn main() {
println('Hello World!')
}Creating a Module - Helper (file1.v)
In plain words: To create your own module, simply make a subfolder (like mod1/) and start the .v files inside it with module mod1.
A module can hold functions that other parts of your program can reuse. In this example, the helper module mod1 exposes a public function called greet.
What happens in this example:
- Creates helper function(s)/method(s):
hello. - Prints the result to the terminal window so you can see the output right away.
module mod1
pub fn hello() {
println('Hello from mod1!')
}Creating a Module - Main (modulebasics.v)
In plain words: To create your own module, simply make a subfolder (like mod1/) and start the .v files inside it with module mod1.
This file acts as the application entry point. It imports the helper module and calls one of its public functions.
What happens in this example:
- Prints the result to the terminal window so you can see the output right away.
module main
fn main() {
println('Hello World!')
}Importing a Module - Helper (file1.v)
In plain words: Use import mod1 at the top of main.v to connect your main program to the code inside the mod1/ folder.
Importing a module gives your program access to its public members. The module name becomes the namespace you use when calling those functions.
What happens in this example:
- Creates helper function(s)/method(s):
hello. - Prints the result to the terminal window so you can see the output right away.
module mod1
pub fn hello() {
println('Hello from mod1!')
}Importing a Module - Main (modulebasics.v)
In plain words: Use import mod1 at the top of main.v to connect your main program to the code inside the mod1/ folder.
The main program can now use the imported module without copying its code into the entry file.
What happens in this example:
- Prints the result to the terminal window so you can see the output right away.
module main
import mod1
fn main() {
println('Hello World!')
}Accessing Module Members - Helper (file1.v)
In plain words: Only functions, structs, and constants marked with pub inside mod1 can be called from outside mod1 (as mod1.hello()).
Not everything in a module should be accessible from outside. In V, pub makes a function available to other modules, while private functions stay inside the module.
What happens in this example:
- Creates helper function(s)/method(s):
hello. - Prints the result to the terminal window so you can see the output right away.
module mod1
pub fn hello() {
println('Hello from mod1!')
}Accessing Module Members - Main (modulebasics.v)
In plain words: Only functions, structs, and constants marked with pub inside mod1 can be called from outside mod1 (as mod1.hello()).
From the main program, you can call the public function, but private helpers remain hidden.
What happens in this example:
- Prints the result to the terminal window so you can see the output right away.
module main
import mod1
fn main() {
mod1.hello()
println('Hello World!')
}Multiple Files (After Refactoring) - Helper 1 (file1.v)
A single module can be split across several files. This makes it easier to keep related helpers organized without changing how the module is imported.
In plain words: All .v files placed in the same folder with the same module name at the top automatically belong to one module and can use each other's functions without needing an import!
What happens in this example:
- Creates helper function(s)/method(s):
hello. - Prints the result to the terminal window so you can see the output right away.
module mod1
pub fn hello() {
println('Hello from mod1!')
}Multiple Files (After Refactoring) - Helper 2 (file2.v)
The second file in the same module can hold additional helper functions. The module still behaves as one logical unit when imported.
In plain words: All .v files placed in the same folder with the same module name at the top automatically belong to one module and can use each other's functions without needing an import!
What happens in this example:
- Creates helper function(s)/method(s):
hello2. - Prints the result to the terminal window so you can see the output right away.
module mod1
fn hello2() {
println('Hello 2 from mod1!')
}Multiple Files (After Refactoring) - Main (modulebasics.v)
In plain words: All .v files placed in the same folder with the same module name at the top automatically belong to one module and can use each other's functions without needing an import!
What happens in this example:
- Prints the result to the terminal window so you can see the output right away.
module main
import mod1
fn main() {
mod1.hello()
println('Hello World!')
}Multiple Files (Before Refactoring) - Helper 1 (file1.v)
In plain words: All .v files placed in the same folder with the same module name at the top automatically belong to one module and can use each other's functions without needing an import!
What happens in this example:
- Creates helper function(s)/method(s):
hello. - Prints the result to the terminal window so you can see the output right away.
module mod1
pub fn hello() {
println('Hello from mod1!')
}Multiple Files (Before Refactoring) - Helper 2 (file2.v)
In plain words: All .v files placed in the same folder with the same module name at the top automatically belong to one module and can use each other's functions without needing an import!
What happens in this example:
- Creates helper function(s)/method(s):
hello2. - Prints the result to the terminal window so you can see the output right away.
fn hello2() {
println('Hello 2 from mod1!')
}
fn main() {
}Multiple Files (Before Refactoring) - Main (modulebasics.v)
In plain words: All .v files placed in the same folder with the same module name at the top automatically belong to one module and can use each other's functions without needing an import!
What happens in this example:
- Prints the result to the terminal window so you can see the output right away.
module main
import mod1
fn main() {
mod1.hello()
println('Hello World!')
}Member Scope (After Refactoring) - Helper 1 (file1.v)
A function marked pub can be called from outside the module, while a private helper can still be used by other functions inside the same module.
In plain words: Files inside the same module folder can call each other's private functions freely, while main.v (in a different module) can only call functions marked with pub.
What happens in this example:
- Code note: hello2 is not a public but accessible within mod1
module mod1
pub fn hello() {
println('Hello from mod1!')
// hello2 is not a public but accessible within mod1
hello2()
}Member Scope (After Refactoring) - Helper 2 (file2.v)
In plain words: Files inside the same module folder can call each other's private functions freely, while main.v (in a different module) can only call functions marked with pub.
What happens in this example:
- Creates helper function(s)/method(s):
hello2. - Prints the result to the terminal window so you can see the output right away.
module mod1
fn hello2() {
println('Hello 2 from mod1!')
}Member Scope (After Refactoring) - Main (modulebasics.v)
In plain words: Files inside the same module folder can call each other's private functions freely, while main.v (in a different module) can only call functions marked with pub.
module main
import mod1
fn main() {
mod1.hello()
}Member Scope (Before Refactoring) - Helper 1 (file1.v)
In plain words: Files inside the same module folder can call each other's private functions freely, while main.v (in a different module) can only call functions marked with pub.
What happens in this example:
- Creates helper function(s)/method(s):
hello. - Prints the result to the terminal window so you can see the output right away.
module mod1
pub fn hello() {
println('Hello from mod1!')
}Member Scope (Before Refactoring) - Helper 2 (file2.v)
In plain words: Files inside the same module folder can call each other's private functions freely, while main.v (in a different module) can only call functions marked with pub.
What happens in this example:
- Creates helper function(s)/method(s):
hello2. - Prints the result to the terminal window so you can see the output right away.
module mod1
fn hello2() {
println('Hello 2 from mod1!')
}Member Scope (Before Refactoring) - Main (modulebasics.v)
In plain words: Files inside the same module folder can call each other's private functions freely, while main.v (in a different module) can only call functions marked with pub.
module main
import mod1
fn main() {
mod1.hello()
mod1.hello2()
}Cyclic Imports - Module 1 Helper (file1.v)
This example shows a circular dependency between two modules. In practice, you should avoid this pattern because it makes the import graph harder to reason about.
In plain words: Module A cannot import Module B if Module B also imports Module A (cyclic import). V forbids circular dependencies so your project stays clean and compiles in under a second.
What happens in this example:
- Creates helper function(s)/method(s):
hello. - Prints the result to the terminal window so you can see the output right away.
module m1
import m2
pub const greet_from_m1 = 'Greetings from m1'
pub fn hello() {
println(m2.greet_from_m2)
}Cyclic Imports - Module 2 Helper (file1.v)
In plain words: Module A cannot import Module B if Module B also imports Module A (cyclic import). V forbids circular dependencies so your project stays clean and compiles in under a second.
What happens in this example:
- Creates helper function(s)/method(s):
hello. - Prints the result to the terminal window so you can see the output right away.
module m2
import m1
pub const greet_from_m2 = 'Greetings from m2'
pub fn hello() {
println(m1.greet_from_m1)
}Cyclic Imports - Main (modulebasics.v)
In plain words: Module A cannot import Module B if Module B also imports Module A (cyclic import). V forbids circular dependencies so your project stays clean and compiles in under a second.
module main
import m1
import m2
fn main() {
m1.hello()
m2.hello()
}Module Init & Cleanup Functions - Config (config.v)
In plain words: If a module defines a private fn init() function, V automatically runs it once before main() startsโhandy for setting up module defaults!
V modules support lifecycle hooks for setting up and tearing down resources. A module's init() function runs when it is first imported, and its cleanup() function runs when the program terminates.
In this helper module config, we define simple hooks to simulate loading configuration details.
module config
pub const version = '1.0.0'
fn init() {
println('Initializing config module...')
}
fn cleanup() {
println('Cleaning up config module...')
}Module Init & Cleanup Functions - Helper (file1.v)
In plain words: If a module defines a private fn init() function, V automatically runs it once before main() startsโhandy for setting up module defaults!
This helper module mod1 imports config, simulates the initialization/release of a C library wrapper, and exposes a public function hello().
init(): A special private function (fn init()) that runs automatically when a module is first imported. It is ideal for one-time initialization, such as preparing C libraries or setting up state.cleanup(): A special private function (fn cleanup()) that executes automatically when the program terminates. It runs in the reverse order of theinit()calls, making it perfect for releasing C library resources or flushing files.
module mod1
import config
pub fn hello() {
println('Hello from mod1! (using config v${config.version})')
}
fn init() {
println('Initializing mod1 module (C library stub initialized)...')
}
fn cleanup() {
println('Cleaning up mod1 module (C library stub released)...')
}Module Init & Cleanup Functions - Main (modulebasics.v)
In plain words: If a module defines a private fn init() function, V automatically runs it once before main() startsโhandy for setting up module defaults!
V is designed to be highly modular. Here is a summary of the core rules governing V modules, and how the program executes:
1. Module Basics & Organization
- Scope: Every file in a directory belongs to the same module. If no module name is specified at the top of the file, it defaults to
main. - Visibility: All elements (structs, functions, constants, etc.) inside a module are visible across all files of that same module, regardless of whether they are marked with
pub. - Names: Module names must be short (ideally under 10 characters) and written in
snake_case. - Circular Imports: Circular imports are strictly forbidden.
2. Module Lookup & `v.mod`
- V uses
v.modfiles as lookup anchors. - The directory containing the nearest
v.modfile is prepended to V's module search path. This enables projects to easily import submodules (e.g.import myapp.common) using relative structure anchors.
3. Special Prototyping Rules for Project Roots
- For the top-level project folder (compiled with
v .), you can have multiple.vfiles belonging to different modules (likemodule mainandmodule abc) in the same directory. - This is a special rule designed to ease prototyping, allowing you to split files easily before moving them to separate directory submodules. In any other non-root directory, all
.vfiles must declare the exact same module name matching the folder name.
4. Lifecycle Hooks (`init` & `cleanup`)
- Neither
init()norcleanup()can be made public (pub). - Single Execution: V calls
init()exactly once when the module is imported, regardless of how many other modules transitively or directly import it. For example,configis imported by bothmod1andmain, but itsinit()runs only once. - Reverse-Order Execution: V calls
cleanup()automatically once at the end of program execution, in the exact reverse order of theirinit()invocations.
module main
import mod1
import config
fn main() {
println('Main function started.')
mod1.hello()
println('Using config directly in main: v${config.version}')
println('Main function ending.')
}Accessing Module Constants - Helper (file1.v)
Constants are shared values that belong to a module. They are great for configuration strings or fixed messages that multiple files can use.
In plain words: Constants marked with pub const inside a module can be read from main by writing module_name.constant_name.
module mod1
pub const greet_msg = 'Greeting from mod1!'Accessing Module Constants - Main (modulebasics.v)
In plain words: Constants marked with pub const inside a module can be read from main by writing module_name.constant_name.
What happens in this example:
- Prints the result to the terminal window so you can see the output right away.
module main
import mod1
fn main() {
println(mod1.greet_msg)
}Accessing Module Structs - Helper (file1.v)
In plain words: To let other modules create a struct and read/change its fields, mark the struct with pub struct and place its public fields under pub: or pub mut:.
What happens in this example:
- Code note: NoteTimeInfo is a struct to store time info of Note
- Code note: Note is a struct with embedding struct NoteTimeInfo along with other fields
module mod1
import time
// NoteTimeInfo is a struct to store time info of Note
pub struct NoteTimeInfo {
pub:
created time.Time = time.now()
pub mut:
due time.Time = time.now().add_days(1)
}
// Note is a struct with embedding struct NoteTimeInfo along with other fields
pub struct Note {
NoteTimeInfo // Embedded Struct
pub:
id int
pub mut:
message string @[required]
status bool
}Accessing Module Structs - Main (modulebasics.v)
In plain words: To let other modules create a struct and read/change its fields, mark the struct with pub struct and place its public fields under pub: or pub mut:.
What happens in this example:
- Defines custom data blueprint(s):
field, field. - Prints the result to the terminal window so you can see the output right away.
module main
import mod1
fn main() {
n := mod1.Note{
id: 1
message: 'Accessing structs of module demo'
}
println('Accessing struct field value Note id: ${n.id}')
println('Accessing embedded struct field value NoteTimeInfo: ${n.NoteTimeInfo}')
}Installing External Packages
V has a built-in package manager called vpm (V Package Manager) that allows you to easily install, update, and manage third-party modules. External packages are hosted on the official V registry at vpm.vlang.io.
How to Install Packages with vpm
To install a package, use the v install command followed by the package identifier (usually in the format author.package_name):
v install xiusin.vredisThis downloads the package and installs it into the V modules directory (typically located at ~/.vmodules/ on Linux/macOS or C:\Users\Username\.vmodules\ on Windows).
Common vpm Commands
- Install a package:
v install author.package_name- Install from a Git repository directly:
v install https://github.com/author/package_name- Update an installed package:
v update author.package_name- Remove/uninstall a package:
v remove author.package_name- Search for packages:
v search queryImporting and Using External Packages
Once a package is installed via vpm, you can import it in your V code just like a standard library module:
import xiusin.vredis
fn main() {
// Code utilizing the external redis package
}Redis Console Demo
In plain words: This lesson (Redis Console Demo) shows a clear, runnable V example from modules/ch11_install_external_packages_and_webview/redis_console_demo/redis_console_demo.v that you can test directly in your terminal or the V Playground.
This example demonstrates how to use the external xiusin.vredis client package in a console application and demonstrates key namespacing with the custom NamespacedRedis helper. It covers:
- Establishing a connection and handling errors gracefully.
- Basic String operations (
set,get,incr,expire,ttl,del). - List operations (
rpush,llen,lrange,lpop). - Hash operations (
hset,hget,hgetall). - Set operations (
sadd,sismember,smembers). - Namespaced Redis helper operations using
NamespacedRedis. - Cleaning up created keys on application exit.
module main
import xiusin.vredis
fn main() {
println('==================================================')
println(' V + Redis Console API Learning Demo ')
println('==================================================')
println('Connecting to local Redis at 127.0.0.1:6379...')
mut r := vredis.new_client(host: '127.0.0.1', port: 6379) or {
eprintln('\n[ERROR] Failed to connect to Redis server: ${err}')
eprintln('Please make sure Redis is running locally on port 6379.')
return
}
defer {
r.close() or {}
println('\n==================================================')
println('Demo completed. Redis connection closed.')
println('==================================================')
}
println('Connected successfully!\n')
// Clean up any old test keys first
r.del('demo:string') or {}
r.del('demo:counter') or {}
r.del('demo:list') or {}
r.del('demo:hash') or {}
r.del('demo:set') or {}
// --- 1. String Operations ---
println('--- 1. String Operations ---')
println('Setting "demo:string" to "Hello V + Redis!"...')
r.set('demo:string', 'Hello V + Redis!') or { panic(err) }
val := r.get('demo:string') or { panic(err) }
println('GET "demo:string" -> "${val}"')
// Increment demo
r.incr('demo:counter') or { panic(err) }
r.incr('demo:counter') or { panic(err) }
counter_val := r.get('demo:counter') or { panic(err) }
println('Counter INCR twice -> "${counter_val}"')
// TTL Demo
println('Setting expiry of 5 seconds on "demo:string"...')
r.expire('demo:string', 5) or { panic(err) }
ttl_val := r.ttl('demo:string') or { panic(err) }
println('TTL remaining: ${ttl_val} seconds\n')
// --- 2. List Operations ---
println('--- 2. List Operations ---')
println('Pushing items to "demo:list" (item_a, item_b, item_c)...')
r.rpush('demo:list', 'item_a') or { panic(err) }
r.rpush('demo:list', 'item_b') or { panic(err) }
r.rpush('demo:list', 'item_c') or { panic(err) }
list_len := r.llen('demo:list') or { panic(err) }
println('List length: ${list_len}')
list_items := r.lrange('demo:list', 0, -1) or { panic(err) }
println('List elements: ${list_items}')
popped := r.lpop('demo:list') or { panic(err) }
println('Popped from left (LPOP): "${popped}"')
list_items_after := r.lrange('demo:list', 0, -1) or { panic(err) }
println('List elements after LPOP: ${list_items_after}\n')
// --- 3. Hash Operations ---
println('--- 3. Hash Operations ---')
println('Setting fields in "demo:hash"...')
r.hset('demo:hash', 'name', 'V Programming Language') or { panic(err) }
r.hset('demo:hash', 'year', '2019') or { panic(err) }
r.hset('demo:hash', 'creator', 'Alex Medvednikov') or { panic(err) }
name_field := r.hget('demo:hash', 'name') or { panic(err) }
println('HGET "demo:hash" "name" -> "${name_field}"')
hash_all := r.hgetall('demo:hash') or { panic(err) }
println('HGETALL "demo:hash" fields & values:')
for k, v in hash_all {
println(' - ${k}: ${v}')
}
println('')
// --- 4. Set Operations ---
println('--- 4. Set Operations ---')
println('Adding members to "demo:set"...')
r.sadd('demo:set', 'apple') or { panic(err) }
r.sadd('demo:set', 'banana') or { panic(err) }
r.sadd('demo:set', 'apple') or { panic(err) } // Duplicate (should be ignored)
is_banana := r.sismember('demo:set', 'banana') or { panic(err) }
is_cherry := r.sismember('demo:set', 'cherry') or { panic(err) }
println('SISMEMBER "demo:set" "banana": ${is_banana}')
println('SISMEMBER "demo:set" "cherry": ${is_cherry}')
set_members := r.smembers('demo:set') or { panic(err) }
println('SMEMBERS "demo:set": ${set_members}\n')
// --- 5. Namespaced Helper Demo ---
println('--- 5. Namespaced Helper Demo ---')
println('Creating a namespaced helper with namespace "app_v1"...')
mut nr := new_namespaced_redis(r, 'app_v1')
println('Setting namespaced key "user_token" (resolved key will be "app_v1:user_token")...')
nr.set('user_token', 'token_abc123') or { panic(err) }
token := nr.get('user_token') or { panic(err) }
println('GET "user_token" via helper -> "${token}"')
// Verify the actual key in Redis (without namespace helper) has the prefix
actual_key := 'app_v1:user_token'
actual_val := r.get(actual_key) or { panic(err) }
println('GET raw "${actual_key}" directly from client -> "${actual_val}"')
// Cleanup namespaced keys
println('Cleaning up namespaced keys...')
nr.del('user_token') or {}
// Clean up test keys
println('\nCleaning up created keys...')
r.del('demo:string') or {}
r.del('demo:counter') or {}
r.del('demo:list') or {}
r.del('demo:hash') or {}
r.del('demo:set') or {}
println('Cleanup done.')
}Redis Console Demo - Helper (redis_helper.v)
In plain words: This lesson (Redis Console Demo - Helper (redis_helper.v)) shows a clear, runnable V example from modules/ch11_install_external_packages_and_webview/redis_console_demo/redis_helper.v that you can test directly in your terminal or the V Playground.
This helper provides a namespaced wrapper struct NamespacedRedis that automatically prefixes all Redis keys with a given namespace (e.g. namespace:key). This is a great pattern for keeping keys organized and avoiding collisions between multiple apps/environments.
module main
import xiusin.vredis
// NamespacedRedis wraps a standard vredis.Redis client and prefixes all keys with a namespace.
// This simplifies multi-tenant or multi-app key separation.
struct NamespacedRedis {
mut:
client &vredis.Redis
pub:
namespace string
}
// new_namespaced_redis creates a new NamespacedRedis helper wrapper.
fn new_namespaced_redis(client &vredis.Redis, namespace string) NamespacedRedis {
return NamespacedRedis{
client: client
namespace: namespace
}
}
// key constructs the final namespaced key.
// E.g. key('mykey') -> 'app1:mykey'
fn (nr NamespacedRedis) key(name string) string {
if nr.namespace == '' {
return name
}
return '${nr.namespace}:${name}'
}
// close closes the connection to the Redis server.
fn (mut nr NamespacedRedis) close() ! {
nr.client.close()!
}
// --- String Operations ---
// set sets a key to a string value.
fn (mut nr NamespacedRedis) set(key string, val string) ! {
nr.client.set(nr.key(key), val)!
}
// get retrieves a string value by key.
fn (mut nr NamespacedRedis) get(key string) !string {
return nr.client.get(nr.key(key))!
}
// incr increments a numeric key.
fn (mut nr NamespacedRedis) incr(key string) ! {
nr.client.incr(nr.key(key))!
}
// expire sets an expiration time (TTL) in seconds on a key.
fn (mut nr NamespacedRedis) expire(key string, seconds int) ! {
nr.client.expire(nr.key(key), seconds)!
}
// ttl returns the remaining Time-To-Live of a key.
fn (mut nr NamespacedRedis) ttl(key string) !int {
return nr.client.ttl(nr.key(key))!
}
// del deletes a key.
fn (mut nr NamespacedRedis) del(key string) ! {
nr.client.del(nr.key(key))!
}
// --- List Operations ---
// rpush appends a value to a list.
fn (mut nr NamespacedRedis) rpush(key string, val string) ! {
nr.client.rpush(nr.key(key), val)!
}
// lrange retrieves a range of elements from a list.
fn (mut nr NamespacedRedis) lrange(key string, start int, stop int) ![]string {
return nr.client.lrange(nr.key(key), start, stop)!
}
// lpop removes and returns the first element of a list.
fn (mut nr NamespacedRedis) lpop(key string) !string {
return nr.client.lpop(nr.key(key))!
}
// llen returns the length of a list.
fn (mut nr NamespacedRedis) llen(key string) !int {
return nr.client.llen(nr.key(key))!
}
// --- Hash Operations ---
// hset sets a field in a hash to a value.
fn (mut nr NamespacedRedis) hset(key string, field string, val string) ! {
nr.client.hset(nr.key(key), field, val)!
}
// hget retrieves a field's value from a hash.
fn (mut nr NamespacedRedis) hget(key string, field string) !string {
return nr.client.hget(nr.key(key), field)!
}
// hgetall retrieves all fields and values of a hash.
fn (mut nr NamespacedRedis) hgetall(key string) !map[string]string {
return nr.client.hgetall(nr.key(key))!
}
// --- Set Operations ---
// sadd adds a member to a set.
fn (mut nr NamespacedRedis) sadd(key string, member string) ! {
nr.client.sadd(nr.key(key), member)!
}
// sismember checks if a member belongs to a set.
fn (mut nr NamespacedRedis) sismember(key string, member string) !bool {
return nr.client.sismember(nr.key(key), member)!
}
// smembers returns all members of a set.
fn (mut nr NamespacedRedis) smembers(key string) ![]string {
return nr.client.smembers(nr.key(key))!
}Redis Namespaced Demo - Helper (redis_helper.v)
In plain words: Shows how a V program imports external packages installed via VPM (like a Redis client or a desktop Webview window) to build connected applications.
What happens in this example:
- Code note: NamespacedRedis wraps a standard vredis.Redis client and prefixes all keys with a namespace.
- Code note: newnamespacedredis creates a new NamespacedRedis helper wrapper.
- Code note: key constructs the final namespaced key.
- Code note: close closes the connection to the Redis server.
module main
import xiusin.vredis
// NamespacedRedis wraps a standard vredis.Redis client and prefixes all keys with a namespace.
struct NamespacedRedis {
mut:
client &vredis.Redis
pub:
namespace string
}
// new_namespaced_redis creates a new NamespacedRedis helper wrapper.
fn new_namespaced_redis(client &vredis.Redis, namespace string) NamespacedRedis {
return NamespacedRedis{
client: client
namespace: namespace
}
}
// key constructs the final namespaced key.
fn (nr NamespacedRedis) key(name string) string {
if nr.namespace == '' {
return name
}
return '${nr.namespace}:${name}'
}
// close closes the connection to the Redis server.
fn (mut nr NamespacedRedis) close() ! {
nr.client.close()!
}
// --- String Operations ---
// set sets a key to a string value.
fn (mut nr NamespacedRedis) set(key string, val string) ! {
nr.client.set(nr.key(key), val)!
}
// get retrieves a string value by key.
fn (mut nr NamespacedRedis) get(key string) !string {
return nr.client.get(nr.key(key))!
}
// incr increments a numeric key.
fn (mut nr NamespacedRedis) incr(key string) ! {
nr.client.incr(nr.key(key))!
}
// expire sets an expiration time (TTL) in seconds on a key.
fn (mut nr NamespacedRedis) expire(key string, seconds int) ! {
nr.client.expire(nr.key(key), seconds)!
}
// ttl returns the remaining Time-To-Live of a key.
fn (mut nr NamespacedRedis) ttl(key string) !int {
return nr.client.ttl(nr.key(key))!
}
// del deletes a key.
fn (mut nr NamespacedRedis) del(key string) ! {
nr.client.del(nr.key(key))!
}
// --- List Operations ---
// rpush appends a value to a list.
fn (mut nr NamespacedRedis) rpush(key string, val string) ! {
nr.client.rpush(nr.key(key), val)!
}
// lrange retrieves a range of elements from a list.
fn (mut nr NamespacedRedis) lrange(key string, start int, stop int) ![]string {
return nr.client.lrange(nr.key(key), start, stop)!
}
// lpop removes and returns the first element of a list.
fn (mut nr NamespacedRedis) lpop(key string) !string {
return nr.client.lpop(nr.key(key))!
}
// llen returns the length of a list.
fn (mut nr NamespacedRedis) llen(key string) !int {
return nr.client.llen(nr.key(key))!
}
// --- Hash Operations ---
// hset sets a field in a hash to a value.
fn (mut nr NamespacedRedis) hset(key string, field string, val string) ! {
nr.client.hset(nr.key(key), field, val)!
}
// hget retrieves a field's value from a hash.
fn (mut nr NamespacedRedis) hget(key string, field string) !string {
return nr.client.hget(nr.key(key), field)!
}
// hgetall retrieves all fields and values of a hash.
fn (mut nr NamespacedRedis) hgetall(key string) !map[string]string {
return nr.client.hgetall(nr.key(key))!
}
// --- Set Operations ---
// sadd adds a member to a set.
fn (mut nr NamespacedRedis) sadd(key string, member string) ! {
nr.client.sadd(nr.key(key), member)!
}
// sismember checks if a member belongs to a set.
fn (mut nr NamespacedRedis) sismember(key string, member string) !bool {
return nr.client.sismember(nr.key(key), member)!
}
// smembers returns all members of a set.
fn (mut nr NamespacedRedis) smembers(key string) ![]string {
return nr.client.smembers(nr.key(key))!
}Redis Namespaced Demo
In plain words: This lesson (Redis Namespaced Demo) shows a clear, runnable V example from modules/ch11_install_external_packages_and_webview/redis_namespaced_demo/redis_namespaced_demo.v that you can test directly in your terminal or the V Playground.
This example provides an easy, dedicated demo showing how to use the NamespacedRedis helper wrapper to manage multiple independent namespaces (like cache and session) over a single underlying Redis connection without key collisions.
module main
import xiusin.vredis
fn main() {
println('==================================================')
println(' V + Redis Namespaced Helper Easy Demo ')
println('==================================================')
println('Connecting to local Redis at 127.0.0.1:6379...')
mut client := vredis.new_client(host: '127.0.0.1', port: 6379) or {
eprintln('\n[ERROR] Failed to connect to Redis server: ${err}')
eprintln('Please make sure Redis is running locally on port 6379.')
return
}
defer {
client.close() or {}
println('\n==================================================')
println('Demo completed. Redis connection closed.')
println('==================================================')
}
println('Connected successfully!\n')
// Create a namespaced client wrapper for "cache"
println('Initializing "cache" namespace wrapper...')
mut cache := new_namespaced_redis(client, 'cache')
// Create another namespaced client wrapper for "session"
println('Initializing "session" namespace wrapper...\n')
mut session := new_namespaced_redis(client, 'session')
// 1. Store value in cache namespace (key will be "cache:user_123")
println('1. Storing data in "cache" namespace (key: "user_123")...')
cache.set('user_123', '{"name": "Alice", "role": "Admin"}') or { panic(err) }
// 2. Store value in session namespace (key will be "session:user_123")
println('2. Storing data in "session" namespace (key: "user_123")...')
session.set('user_123', 'active_session_token_xyz987') or { panic(err) }
println('\n--- Retrieval ---')
// 3. Retrieve values using the namespace helpers
cache_val := cache.get('user_123') or { panic(err) }
session_val := session.get('user_123') or { panic(err) }
println('Retrieved from cache: "${cache_val}"')
println('Retrieved from session: "${session_val}"')
println('\n--- Verification (Direct Raw Lookups) ---')
// 4. Retrieve values using the raw client directly to show the actual keys stored
raw_cache := client.get('cache:user_123') or { panic(err) }
raw_session := client.get('session:user_123') or { panic(err) }
println('Raw key "cache:user_123" directly: "${raw_cache}"')
println('Raw key "session:user_123" directly: "${raw_session}"')
// Cleanup
println('\nCleaning up keys...')
cache.del('user_123') or {}
session.del('user_123') or {}
println('Cleanup done.')
}Redis Webview Demo
In plain words: Shows how a V program imports external packages installed via VPM (like a Redis client or a desktop Webview window) to build connected applications.
What happens in this example:
- Code note: Embed the HTML, CSS, and JS file directly into the binary
module main
import json2
import ttytm.webview
import xiusin.vredis
struct KeyInfo {
name string
@type string
ttl int
}
struct KeyDetail {
mut:
name string
@type string
ttl int
value string
list_val []string
hash_val map[string]string
}
struct ConnectStatus {
status string
host string
port int
version string
keys_count int
}
// Embed the HTML, CSS, and JS file directly into the binary
const html_file = $embed_file('index.html')
const html = html_file.to_string()
fn connect_redis() !&vredis.Redis {
return vredis.new_client(host: '127.0.0.1', port: 6379)
}
fn redis_connect_status(e &webview.Event) !string {
mut client := connect_redis() or {
status_info := ConnectStatus{
status: 'disconnected'
host: '127.0.0.1'
port: 6379
version: ''
keys_count: 0
}
return json2.encode(status_info)
}
defer {
client.close() or {}
}
mut version := 'Unknown'
info := client.send('INFO', 'server') or {
count := client.dbsize() or { 0 }
status_info := ConnectStatus{
status: 'connected'
host: '127.0.0.1'
port: 6379
version: 'Redis (Unknown)'
keys_count: count
}
return json2.encode(status_info)
}
for line in info.split_into_lines() {
if line.starts_with('redis_version:') {
version = line.all_after('redis_version:').trim_space()
break
}
}
count := client.dbsize() or { 0 }
status_info := ConnectStatus{
status: 'connected'
host: '127.0.0.1'
port: 6379
version: 'Redis ${version}'
keys_count: count
}
return json2.encode(status_info)
}
fn redis_list_keys(e &webview.Event) !string {
pattern := e.get_arg[string](0) or { '*' }
filter_pattern := if pattern == '' { '*' } else { pattern }
mut client := connect_redis()!
defer {
client.close() or {}
}
keys := client.keys(filter_pattern) or { []string{} }
mut items := []KeyInfo{}
for key in keys {
typ := client.key_type(key) or { 'unknown' }
ttl := client.ttl(key) or { -1 }
items << KeyInfo{
name: key
@type: typ
ttl: ttl
}
}
return json2.encode(items)
}
fn redis_get_key_detail(e &webview.Event) !string {
key := e.get_arg[string](0)!
mut client := connect_redis()!
defer {
client.close() or {}
}
typ := client.key_type(key) or { 'none' }
ttl := client.ttl(key) or { -1 }
mut detail := KeyDetail{
name: key
@type: typ
ttl: ttl
value: ''
list_val: []string{}
hash_val: map[string]string{}
}
match typ {
'string' {
detail.value = client.get(key) or { '' }
}
'list' {
detail.list_val = client.lrange(key, 0, -1) or { []string{} }
}
'set' {
detail.list_val = client.smembers(key) or { []string{} }
}
'hash' {
detail.hash_val = client.hgetall(key) or { map[string]string{} }
}
'zset' {
detail.list_val = client.zrange(key, 0, -1) or { []string{} }
}
else {}
}
return json2.encode(detail)
}
fn redis_set_string(e &webview.Event) !string {
mut client := connect_redis()!
defer {
client.close() or {}
}
key := e.get_arg[string](0)!
val := e.get_arg[string](1)!
ttl := e.get_arg[int](2)!
client.set(key, val)!
if ttl > 0 {
client.expire(key, ttl)!
} else if ttl == -1 {
client.persist(key) or {}
}
return 'ok'
}
fn redis_set_list(e &webview.Event) !string {
mut client := connect_redis()!
defer {
client.close() or {}
}
key := e.get_arg[string](0)!
vals_json := e.get_arg[string](1)!
ttl := e.get_arg[int](2)!
vals := json2.decode[[]string](vals_json)!
client.del(key) or {}
for val in vals {
client.rpush(key, val)!
}
if ttl > 0 {
client.expire(key, ttl)!
} else if ttl == -1 {
client.persist(key) or {}
}
return 'ok'
}
fn redis_set_hash(e &webview.Event) !string {
mut client := connect_redis()!
defer {
client.close() or {}
}
key := e.get_arg[string](0)!
hash_json := e.get_arg[string](1)!
ttl := e.get_arg[int](2)!
fvs := json2.decode[map[string]string](hash_json)!
client.del(key) or {}
for field, val in fvs {
client.hset(key, field, val)!
}
if ttl > 0 {
client.expire(key, ttl)!
} else if ttl == -1 {
client.persist(key) or {}
}
return 'ok'
}
fn redis_set_set(e &webview.Event) !string {
mut client := connect_redis()!
defer {
client.close() or {}
}
key := e.get_arg[string](0)!
vals_json := e.get_arg[string](1)!
ttl := e.get_arg[int](2)!
vals := json2.decode[[]string](vals_json)!
client.del(key) or {}
for val in vals {
client.sadd(key, val)!
}
if ttl > 0 {
client.expire(key, ttl)!
} else if ttl == -1 {
client.persist(key) or {}
}
return 'ok'
}
fn redis_del_key(e &webview.Event) !string {
mut client := connect_redis()!
defer {
client.close() or {}
}
key := e.get_arg[string](0)!
client.del(key)!
return 'ok'
}
fn redis_flush_db(e &webview.Event) !string {
mut client := connect_redis()!
defer {
client.close() or {}
}
client.flushdb()!
return 'ok'
}
fn main() {
mut w := webview.create(debug: true)
defer {
w.destroy()
}
w.set_title('V + Redis GUI Dashboard')
w.set_size(1080, 720, .@none)
// Bindings
w.bind_opt[string]('redis_connect_status', redis_connect_status)
w.bind_opt[string]('redis_get_keys', redis_get_keys)
w.bind_opt[string]('redis_get_key_detail', redis_get_key_detail)
w.bind_opt[string]('redis_set_string', redis_set_string)
w.bind_opt[string]('redis_set_list', redis_set_list)
w.bind_opt[string]('redis_set_hash', redis_set_hash)
w.bind_opt[string]('redis_set_set', redis_set_set)
w.bind_opt[string]('redis_del_key', redis_del_key)
w.bind_opt[string]('redis_flush_db', redis_flush_db)
w.set_html(html)
w.run()
}Webview Demo
In plain words: Shows how a V program imports external packages installed via VPM (like a Redis client or a desktop Webview window) to build connected applications.
What happens in this example:
- Code note: Call the bound V function "greetfromv" asynchronously
- Code note: V binding function. Must take &webview.Event and can return a type (like string).
- Code note: 1. Retrieve the argument passed from JavaScript (at index 0)
- Code note: 2. We can run custom JavaScript on the webview page from V
module main
import ttytm.webview
const html = '
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: linear-gradient(135deg, #1e1e2f 0%, #111119 100%);
color: #f8f8f2;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
margin: 0;
user-select: none;
}
.container {
text-align: center;
background: rgba(255, 255, 255, 0.05);
padding: 30px;
border-radius: 12px;
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.3);
backdrop-filter: blur(4px);
border: 1px solid rgba(255, 255, 255, 0.1);
}
h1 {
margin-bottom: 20px;
font-size: 2.2rem;
color: #50fa7b;
}
input {
padding: 10px 15px;
font-size: 1rem;
border-radius: 6px;
border: 1px solid #6272a4;
background-color: #282a36;
color: #f8f8f2;
margin-right: 10px;
outline: none;
}
button {
padding: 10px 20px;
font-size: 1rem;
font-weight: bold;
color: #282a36;
background-color: #50fa7b;
border: none;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
}
button:hover {
background-color: #8be9fd;
transform: translateY(-1px);
}
#result {
margin-top: 25px;
font-size: 1.1rem;
min-height: 25px;
color: #f1fa8c;
}
</style>
</head>
<body>
<div class="container">
<h1>V + Webview Binding</h1>
<input type="text" id="userInput" placeholder="Enter message for V..." value="Hello from JS!">
<button onclick="sendToV()">Send to V</button>
<div id="result">Waiting for action...</div>
</div>
<script>
async function sendToV() {
const input = document.getElementById("userInput").value;
const resultDiv = document.getElementById("result");
resultDiv.innerText = "Calling V function...";
try {
// Call the bound V function "greet_from_v" asynchronously
const res = await window.greet_from_v(input);
resultDiv.innerText = res;
} catch (err) {
resultDiv.innerText = "Error: " + err;
}
}
</script>
</body>
</html>
'
// V binding function. Must take &webview.Event and can return a type (like string).
fn greet_from_v(e &webview.Event) string {
// 1. Retrieve the argument passed from JavaScript (at index 0)
msg := e.get_arg[string](0) or { 'No arguments passed' }
println('V side: Received from JS: ${msg}')
// 2. We can run custom JavaScript on the webview page from V
e.eval('console.log("V successfully invoked eval in JS context!");')
// 3. Return string back to the JS Promise resolver
return 'V responds: "Message received: ${msg}"'
}
fn main() {
// Initialize Webview
mut w := webview.create(debug: true)
w.set_title('V Webview Binding Demo')
w.set_size(600, 450, .@none)
// Bind V function "greet_from_v" to JS window.greet_from_v
w.bind('greet_from_v', greet_from_v)
// Load the HTML content
w.set_html(html)
// Run the main loop
w.run()
}Packaging Webview as macOS Binaries
Packaging Webview as macOS Binaries
V can run JavaScript, HTML, and CSS under the hood inside its lightweight webview bindings to build impressive cross-platform desktop GUIs. Once your webview-based application is ready to ship, you can package it as a standalone, production-ready macOS app bundle.
To make this seamless, you can use vlang_macos_webview_app_template. This is a pure-V template framework featuring a specialized build tool build.vsh that runs native macOS utilities (such as sips and iconutil) to assemble, compile, and structure your app without any Node or Bun JavaScript toolchain requirements.
Step-by-Step Packaging Guide
To package your webview app, follow the instructions below:
1. Prerequisites and Installation
Ensure you have the V compiler installed and ttytm.webview registered:
# Install the webview library
v install ttytm.webview
# Clone the packaging template repository
git clone https://github.com/codecaine-zz/vlang_macos_webview_app_template.git
cd vlang_macos_webview_app_template2. Running Locally under Development
To execute your webview application inline without compilation to verify behaviors:
v run main.v3. Packaging as a Default macOS App Bundle
To compile main.v with full release-mode optimizations (-prod) and package it using the default name and default wave icon:
v run build.vshThis compiles your application and structures:
dist/Vlang Macos Webview App Template.app4. Custom App Packaging (Custom Name, Icon, and ID)
You can customize the compilation by specifying command flags to customize display names, custom bundle identifiers, and apply any of the 101 built-in glassmorphism workspace icons:
v run build.vsh main.v --name "My custom App" --icon resources/developer.png --identifier "com.example.myapp"Available build parameters:
-i, --icon <path>: Path to a PNG icon. Defaults to resources/icon.png or icon.png.-n, --name <name>: Custom display name for the .app bundle wrapper.-d, --identifier <id>: CFBundleIdentifier mapping (e.g. com.example.id).-v, --version <version>: App version (defaults to version in v.mod, or 1.0.0).-o, --out <dir>: Custom destination output directory (defaults to dist).
Example building with local premium glassmorphism icons:
# Build an IDE app using the Developer icon template
v run build.vsh --name "Code Studio" --icon resources/developer.png
# Build a database tool using the Database Admin icon
v run build.vsh --name "DB Browser" --icon resources/database_admin.png
# Build a task planner using the Kanban Board icon
v run build.vsh --name "Task Board" --icon resources/kanban_board.png5. Running the Packaged macOS App
Once built, you can run and distribute your app bundle by:
- Double-clicking the .app bundle inside Finder (located in the dist/ output directory).
- Launching it from the terminal:
open "dist/Vlang Macos Webview App Template.app"Using premium templates like this ensures your compiled V webview apps have zero runtime bloat and deliver a sleek, fully native feel matches Apple's premium macOS Sequoia glassmorphism specifications.
Selective Imports
In plain words: Writing import math { max, min } imports only the specific functions you list so you can call max(1, 2) directly without typing math.max.
V permits you to selectively import specific public functions and types directly from a module by using the syntax import module_name { symbol1, symbol2 }. This allows calling those symbols directly without the module prefix. Note that selective imports are not permitted for module constants, which must always be prefixed.
module main
import os { input, user_os }
fn main() {
println('=== Selective Imports ===')
// We can use the imported functions directly without os. prefix:
name := 'Ada'
println('Hello, ${name}!')
current_os := user_os()
println('Your OS is ${current_os}.')
assert current_os.len > 0
}Module Hierarchy
File locations: modules/ch13_module_hierarchy/abc/file1.v, modules/ch13_module_hierarchy/abc/def/file2.v, modules/ch13_module_hierarchy/modulebasics.v
Module Hierarchy
Modules in V map directly to the directory hierarchy. However, nested directory hierarchies are resolved in a flattened namespace. A submodule located in abc/def/source.v is declared with module def (not module abc.def), but must be imported via import abc.def and its public symbols are called using a single prefix: def.func().
Module Helper (`abc/file1.v`):
module abc
pub fn hello_abc() {
println('Hello from abc!')
}Submodule Helper (`abc/def/file2.v`):
module def
pub fn hello_def() {
println('Hello from def!')
}Main Entry (`modulebasics.v`):
module main
import abc
import abc.def
fn main() {
println('=== Module Hierarchy ===')
abc.hello_abc()
def.hello_def() // Call with def, not abc.def
}Module Import Aliasing
File locations: modules/ch14_module_import_aliasing/mymod/sha256/sha256.v, modules/ch14_module_import_aliasing/modulebasics.v
Module Import Aliasing
When you have module naming conflicts (for example, importing two different modules named sha256), you can resolve them by aliasing one or both using the as keyword:
Custom SHA256 Helper (`mymod/sha256/sha256.v`):
module sha256
pub fn sum(data []u8) string {
return 'mock_sha256_sum_for_aliasing_demo'
}Main Entry (`modulebasics.v`):
module main
import crypto.sha256
import mymod.sha256 as mysha256
fn main() {
println('=== Module Import Aliasing ===')
// Use the standard crypto.sha256:
v_hash := sha256.sum('hi'.bytes()).hex()
// Use our aliased mymod.sha256:
my_hash := mysha256.sum('hi'.bytes())
println('Standard hash: ${v_hash}')
println('Aliased mymod hash: ${my_hash}')
assert my_hash == 'mock_sha256_sum_for_aliasing_demo'
}Chapter 10 Writing Tests in V
Plain-English Chapter Guide (Start Here if You Are New to Coding!)
- The Big Idea in Simple Words: How do you know your code still works after you change something? Instead of manually clicking around and typing test inputs every time, you can write small automatic checks called tests! In V, testing is effortless: you use the word
assert(which just means "make sure this is true"), put your test functions (fn test_something()) in a file ending with_test.v, and runv test .. - Words to Know:
assert(assert 2 + 2 == 4): Checks that a condition istrue. If it isfalse, V stops and shows you the exact values on both sides!- Test File (
*_test.v): Any file ending in_test.vthat V runs automatically when you typev test. - Test Function (
fn test_add()): A function starting withtest_that checks one piece of your program.
Quick Access โ Jump to any lesson in this chapter
Below is an index of all code examples in this chapter. You can use these links to jump directly to any specific code example:
Assertions & Unit Testing
V has testing built directly into the compiler. This chapter explains how to write test files, use assertions, set up test suites with setup/teardown methods, and run test suites.
Under the Hood: Test Discovery & The Assertion Engine
๐ง Optional Deep Dive โ Click to Expand / Collapse
Plain-English Summary: In simple words: When an assert check fails in V, the compiler automatically prints out the exact left-hand and right-hand values for you so you can spot the mistake immediately.
V comes with a built-in, zero-configuration testing harness that makes test-driven development seamless.
Zero-Configuration Test Discovery
When you execute v test . or v test path/to/module:
- The compiler traverses the directory tree and detects every file ending with
_test.v. - For each test file, V compiles a dedicated test executable containing all functions prefixed with
test_(e.g.,fn test_addition()). - Each test function is executed in isolation. If all assertions pass, the test runner outputs a green confirmation banner with timing metrics.
Compile-Time Assertion Inspection
In standard C or other languages, assertions are simple runtime checks that print an opaque error when false.
In V, assert is an advanced compile-time feature:
assert user.age == 25When compiling this statement:
* The compiler captures the Abstract Syntax Tree (AST) of the left-hand expression (user.age) and right-hand expression (25).
* If the assertion fails at runtime, V intercepts the failure and prints an informative, colorized terminal diff:
Assertion failed:
File: user_test.v:18
Expected: user.age == 25
Left value: user.age = 24
Right value: 25Test Harness Setup and Teardown (`test_main`)
For integration tests requiring database initialization or temporary file creation, you can define a test_main() function in your _test.v file:
fn test_main() {
// Global setup: connect to test database
setup_test_db()
// Run all test_* functions in this file
test_run_all()
// Global teardown: clean up test database
teardown_test_db()
}Assertions & Unit Testing
Assert Demo
In plain words: This testing example (Assert Demo) shows how V's built-in assert and v test runner verify that your functions return the right answers automatically.
What happens in this example:
- Uses
assertto automatically verify that the expected condition istrue. - Prints the result to the terminal window so you can see the output right away.
module main
fn main() {
println('1st assert')
msg := 'hello there!'
assert msg.contains('hello') // true
println('2nd assert')
assert 'apple' == 'orange' // stops execution
println('done')
}Asserts with an Extra Message
In plain words: Add a comma and an explanation string after assert (assert x > 0, 'x must be positive') to print a helpful human message if the check ever fails.
V allows appending a custom error message to assert statements using a comma: assert condition, 'custom error message'. When the assertion fails, this message will be printed to help troubleshoot the failure.
module main
fn main() {
println('=== Assert with Message ===')
for i in 0 .. 5 {
// This assertion is true for all i < 5, but demonstrates how to supply a message
assert i * 2 < 10, 'assertion failed for i: ${i}'
}
println('All assertions passed!')
}Asserts That Do Not Abort Your Program
In plain words: Adding @[assert_continues] above a test function tells V to keep running the rest of the checks in that function even if one assert fails.
By default, an assertion failure immediately terminates the running program. If you are prototyping or running tests where you want all assertion failures to be reported without halting execution, you can tag the containing function with the @[assert_continues] attribute:
module main
@[assert_continues]
fn check_value(ii int) {
assert ii == 2
}
fn main() {
println('=== Assert Continues ===')
for i in 0 .. 4 {
check_value(i)
}
println('Finished running!')
}Simple Test - Before (demo_test.v)
In plain words: This testing example (Simple Test - Before) shows how V's built-in assert and v test runner verify that your functions return the right answers automatically.
What happens in this example:
- Creates helper function(s)/method(s):
test_first. - Uses
assertto automatically verify that the expected condition istrue.
fn test_first() {
assert 2 != 2
}Simple Test - After (demo_test.v)
In plain words: This testing example (Simple Test - After) shows how V's built-in assert and v test runner verify that your functions return the right answers automatically.
What happens in this example:
- Creates helper function(s)/method(s):
test_first. - Uses
assertto automatically verify that the expected condition istrue.
fn test_first() {
assert 2 == 2
}Testsuite Demo Test
In plain words: This testing example (Testsuite Demo Test) shows how V's built-in assert and v test runner verify that your functions return the right answers automatically.
What happens in this example:
- Creates helper function(s)/method(s):
testsuite_begin, test_env_foo_has_value_bar, testsuite_end. - Uses
assertto automatically verify that the expected condition istrue. - Prints the result to the terminal window so you can see the output right away.
import os
fn testsuite_begin() {
os.setenv('foo', 'bar', true)
println('About to start executing all tests')
}
fn test_env_foo_has_value_bar() {
println('Executing test')
// arrange
inp := 'foo'
expected := 'bar'
// act
actual := os.getenv(inp)
// assert
assert actual == expected
}
fn testsuite_end() {
os.unsetenv('foo')
println('Finished executing all tests')
}Testing Optional Return Functions (demo_test.v)
In plain words: This testing example (Testing Optional Return Functions) shows how V's built-in assert and v test runner verify that your functions return the right answers automatically.
What happens in this example:
- Creates helper function(s)/method(s):
greet, test_greet_given_a_name, test_greet_propagates_error, test_greet_when_empty. - Includes an
or { ... }safety block to handle potential errors or missing values calmly. - Uses
assertto automatically verify that the expected condition istrue.
fn greet(name string) !string {
if name != '' {
return 'Hello ${name}!'
}
return error('name not provided')
}
fn test_greet_given_a_name() {
exp := 'Hello Pavan!'
assert (greet('Pavan') or { err.msg() }) == exp
}
fn test_greet_propagates_error() ! {
res := greet('Pavan')!
assert res == 'Hello Pavan!'
}
fn test_greet_when_empty() {
exp := 'name not provided'
assert (greet('') or { err.msg() }) == exp
}Greet
In plain words: This testing example (Greet) shows how V's built-in assert and v test runner verify that your functions return the right answers automatically.
What happens in this example:
- Creates helper function(s)/method(s):
greet. - Prints the result to the terminal window so you can see the output right away.
module main
fn greet(name string) string {
return 'Hello ${name}!'
}
fn main() {
msg := greet('Bob')
println(msg)
}Greet Test
In plain words: This testing example (Greet Test) shows how V's built-in assert and v test runner verify that your functions return the right answers automatically.
What happens in this example:
- Creates helper function(s)/method(s):
test_greet. - Uses
assertto automatically verify that the expected condition istrue.
module main
fn test_greet() {
// Arrange
name := 'Bob'
exp_msg := 'Hello Bob!'
// Act
act_msg := greet(name)
// Assert
assert act_msg == exp_msg
assert act_msg.contains(name)
}Main Test
In plain words: This testing example (Main Test) shows how V's built-in assert and v test runner verify that your functions return the right answers automatically.
What happens in this example:
- Creates helper function(s)/method(s):
test_hello. - Uses
assertto automatically verify that the expected condition istrue.
module main
import mod1
fn test_hello() {
// arrange
exp := 'Hello from mod1!'
// act
act := mod1.hello()
// assert
assert act == exp
assert mod1.hello().contains('Hello')
}Testing Program Modules - Helper (file1.v)
In plain words: This testing example (Testing Program Modules - Helper) shows how V's built-in assert and v test runner verify that your functions return the right answers automatically.
What happens in this example:
- Creates helper function(s)/method(s):
hello.
module mod1
pub fn hello() string {
return 'Hello from mod1!'
}Mod1 Test
In plain words: This testing example (Mod1 Test) shows how V's built-in assert and v test runner verify that your functions return the right answers automatically.
What happens in this example:
- Creates helper function(s)/method(s):
test_hello. - Uses
assertto automatically verify that the expected condition istrue.
module mod1
fn test_hello() {
// arrange
exp := 'Hello from mod1!'
// act
act := hello()
// assert
assert act == exp
}Modulebasics
In plain words: This testing example (Modulebasics) shows how V's built-in assert and v test runner verify that your functions return the right answers automatically.
What happens in this example:
- Prints the result to the terminal window so you can see the output right away.
module main
import mod1
fn main() {
res := mod1.hello()
println(res)
}Chapter 11 Concurrency and Channels
Plain-English Chapter Guide (Start Here if You Are New to Coding!)
- The Big Idea in Simple Words: Normally, a program does one thing at a time, like a single chef cooking one dish after another. Concurrency lets your program hire extra helpers to work at the same time in the background (for example, downloading three files at once while keeping the screen responsive). In V, starting a background helper is as easy as putting the word
spawnin front of a function call, and helpers pass messages safely to each other through a channel (chan), which works like a conveyor belt or mailbox! - Words to Know:
spawn(spawn do_work()): Starts a function running in the background right away without waiting for it to finish.- Thread Handle (
h := spawn ...andh.wait()): A ticket that lets you wait (h.wait()) until a background job finishes and get its result. - Channel (
ch := chan int{}): A safe conveyor belt (<-) for sending data from one background worker to another. - Buffered vs Unbuffered Channel: An unbuffered channel waits until both sender and receiver are ready for a direct hand-off; a buffered channel (
cap: 5) has a mailbox slot that can hold up to 5 items before the sender has to wait. sharedandlock: Protects a variable shared by multiple workers so only one worker can change it at a time.
Quick Access โ Jump to any lesson in this chapter
Below is an index of all code examples in this chapter. You can use these links to jump directly to any specific code example:
Channels & Communication
- Unbuffered Channel
- Define Buffered Channel (buffered_channel.v)
- Push Buffered
- Push Unbuffered
- Pop
- Channel Properties
- Try Push Unbuffered
- Try Push Buffered
- Try Pop
- Close
- Defer Close
- Blocking Channels
- Dealing Before
- Dealing After
- Unbuffered Sync Before (sync_before.v)
- Unbuffered Sync After (sync_after.v)
- Understanding Buffered Channel (buffered_channel.v)
- Coroutines Communication
- Buffered Sync Before (sync_before.v)
- Buffered Sync After (sync_after.v)
- Channel Select Before
- Channel Select
V-Routines & Concurrency
V makes concurrent programming easy and safe. This chapter covers spawning threads using spawn, communicating safely between threads using channels, and sharing state safely using shared and lock primitives.
Under the Hood: V-Routines, Thread Pooling & Channel Ring Buffers
๐ง Optional Deep Dive โ Click to Expand / Collapse
Plain-English Summary: In simple words: spawn runs a function on a real system thread, and a channel (chan) acts as a thread-safe waiting line where workers can drop off and pick up data without stepping on each other's toes.
V provides modern concurrency primitives designed for high throughput, safe data exchange, and lock-free execution patterns where possible.
The `spawn` Keyword & Task Scheduling
To run a function concurrently, use the spawn keyword:
handle := spawn fetch_data(endpoint)Under the hood:
spawnschedules the function call onto an internal worker thread pool.- It returns a
thread Thandle (whereTis the return type of the spawned function). - Calling
result := handle.wait()synchronizes with the concurrent task and retrieves its return value, blocking only until the task completes.
Channel (`chan T`) Ring Buffer Architecture
Channels provide safe, synchronized communication between concurrent tasks:
Sender ---> [ Ring Buffer Slot 0 | Slot 1 | Slot 2 | ... ] ---> Receiver
^ ^
Write Pointer Read Pointer* Unbuffered Channels (chan int{cap: 0}): Synchronous rendezvous. A send operation blocks until a receiver is ready to read, and a read operation blocks until a sender transmits.
* Buffered Channels (chan int{cap: 100}): Circular ring buffer allocated in memory. Senders write without blocking until the buffer reaches capacity; receivers read without blocking until the buffer is empty.
* Thread Safety: Channel operations are synchronized using internal atomic locks and condition variables, preventing race conditions.
Multiplexing with `select`
The select statement enables non-blocking or multi-channel synchronization:
select {
msg := <-ch1 {
println('Received from ch1: ${msg}')
}
msg := <-ch2 {
println('Received from ch2: ${msg}')
}
> 500 * time.millisecond {
println('Timeout waiting for channels')
}
else {
println('No channel ready, continuing without blocking')
}
}Shared Memory Concurrency & `lock` Blocks
When multiple concurrent tasks need to share mutable data, declare the struct with shared:
struct State {
mut:
counter int
}
mut state := shared State{ counter: 0 }
// In concurrent task:
lock state {
state.counter++ // Exclusive write lock
}
rlock state {
println('Current count: ${state.counter}') // Shared read lock
}Under the hood, shared variables are paired with a POSIX read-write lock (pthread_rwlock_t), guaranteeing thread safety without manual mutex lifecycle management.
Channels & Communication
Unbuffered Channel
In plain words: Creates a basic unbuffered channel (chan int{}) where sending (<-) and receiving wait for each other to hand off values directly.
Unbuffered channels in V have a capacity of 0. Sending data into an unbuffered channel blocks the sender thread until a receiver thread is ready to pop the data. This provides a strong synchronization point between execution threads.
module main
fn main() {
// 1. Declare an unbuffered channel of type 'int'.
// In V, channels are declared using the `chan` keyword followed by the type.
// An empty initializer `{}` defaults the capacity (`cap`) to 0.
uc := chan int{}
// 2. Query the capacity of the channel.
// For unbuffered channels, the capacity is always 0.
// This means any send operation (pushing data) will block the sending thread
// until another thread is actively reading (popping data) from the channel.
println('Unbuffered channel capacity: ${uc.cap}') // Outputs: 0
// 3. Print the type name of the channel.
// V's `typeof().name` provides runtime type reflection names.
println('Type of channel: ${typeof(uc).name}') // Outputs: chan int
}Define Buffered Channel (buffered_channel.v)
In plain words: Creates a buffered channel (chan string{cap: 2}) that can hold up to 2 queued messages in memory before senders have to wait.
Buffered channels in V are initialized with a specific capacity. The sender thread can push elements into the channel without blocking as long as the buffer is not completely full. Once the buffer is full, subsequent send operations will block until elements are read by another thread.
module main
fn main() {
// 1. Declare a buffered channel of type 'string' with a capacity of 2.
// We specify capacity using the `cap` initialization field.
bc := chan string{cap: 2}
// 2. Query the capacity of the channel.
// For buffered channels, this returns the size of the buffer.
// The sending thread will NOT block when pushing items into the channel
// until the buffer is completely full (in this case, containing 2 elements).
println('Buffered channel capacity: ${bc.cap}') // Outputs: 2
// 3. Print the type name of the channel.
println('Type of channel: ${typeof(bc).name}') // Outputs: chan string
}Push Buffered
In plain words: With a buffered channel (chan int{cap: 1}), the channel has a built-in mailbox slot, so you can push a value in (ch <- 51) right away without needing another thread waiting at that exact millisecond.
What happens in this example:
- Uses V concurrency (
spawn/chan) to run or coordinate work safely across threads. - Prints the result to the terminal window so you can see the output right away.
fn main() {
ch := chan int{cap: 1}
ch <- 51
println(ch)
}Push Unbuffered
In plain words: An unbuffered channel (chan int{}) has 0 storage slotsโpushing a value (ch <- val) waits until another thread is ready to receive it, like a direct hand-to-hand pass.
What happens in this example:
- Uses V concurrency (
spawn/chan) to run or coordinate work safely across threads. - Prints the result to the terminal window so you can see the output right away.
fn main() {
ch := chan int{}
ch <- 51
println(ch) // doesn't prints, due to blocking behavior of unbuffered channels
}Pop
In plain words: Writing val := <-ch (pop / receive) pulls the next item out of a channel ch so your code can use it.
What happens in this example:
- Uses V concurrency (
spawn/chan) to run or coordinate work safely across threads. - Prints the result to the terminal window so you can see the output right away.
fn main() {
ch := chan int{cap: 1}
ch <- 51
println('channel after push: ${ch.str()}')
println('popping value out of the channel and storing it in immutable variable x')
x := <-ch
println('value of x: ${x}')
println('channel after pop: ${ch.str()}')
}Deep Dive Explanation: Channels & Basic Operations
1. Unbuffered vs. Buffered Channels
- Unbuffered Channels (
cap: 0): Initialized viachan T{}. They have no intermediate storage. Any send operation (ch <- value) blocks the sender until a receiver is ready to pop the data (<-ch), and vice versa.
Deadlock Risk: In the Push Unbuffered example, calling ch <- 51 in the main thread without spawning a concurrent reader thread blocks the program permanently, resulting in a thread deadlock.
- Buffered Channels (
cap > 0): Initialized viachan T{cap: N}. They can hold up toNitems in a queue. Pushing to a buffered channel does not block as long as the current queue size is less thanN. It only blocks when the buffer is full (ch.len == N). Popping blocks only when the buffer is empty (ch.len == 0).
2. Channel Operations & Metadata Fields
- Pushing (
<-): Sends data to the channel. Format:channel_var <- data. - Popping (
<-): Receives data from the channel. Format:variable := <-channel_var. - Properties:
.cap: The static, defined capacity of the channel (0 for unbuffered)..len: The number of currently buffered elements waiting to be popped..closed: A boolean indicating if the channel has been shut down viaclose(ch).
Channel Properties
In plain words: Every channel has .cap (maximum capacity of its buffer), .len (how many items are currently waiting inside), and .closed (whether the channel has been closed).
What happens in this example:
- Uses V concurrency (
spawn/chan) to run or coordinate work safely across threads. - Prints the result to the terminal window so you can see the output right away.
fn main() {
b := chan string{cap: 2}
b <- 'hello'
println('capacity: ${b.cap}')
println('length: ${b.len}')
println('closed: ${b.closed}')
}Try Push Unbuffered
In plain words: .try_push(val) attempts to send a value into a channel without waiting. If the channel is full or nobody is listening, it immediately returns a status like .not_ready instead of freezing your thread!
What happens in this example:
- Uses V concurrency (
spawn/chan) to run or coordinate work safely across threads. - Prints the result to the terminal window so you can see the output right away.
fn main() {
v := 'hi'
ch := chan string{} // unbuffered channel
res := ch.try_push(v)
println(res) // not_ready
}Try Push Buffered
In plain words: .try_push(val) attempts to send a value into a channel without waiting. If the channel is full or nobody is listening, it immediately returns a status like .not_ready instead of freezing your thread!
What happens in this example:
- Uses V concurrency (
spawn/chan) to run or coordinate work safely across threads. - Prints the result to the terminal window so you can see the output right away.
fn main() {
x := 'hello'
ch := chan string{cap: 2}
for {
status := ch.try_push(x)
if status == .success {
println('Channel length: ${ch.len}')
} else {
println('channel status: ${status}')
break
}
}
}Try Pop
In plain words: .try_pop(mut val) checks if there is an item ready to grab from the channel right now; if the channel is empty, your program keeps moving instead of waiting.
What happens in this example:
- Uses
mutso the variable or collection is allowed to be updated after creation. - Uses V concurrency (
spawn/chan) to run or coordinate work safely across threads. - Prints the result to the terminal window so you can see the output right away.
fn main() {
ch := chan int{cap: 1}
mut x, mut y := 0, 0
ch <- 101
mut status := ch.try_pop(mut x)
println('try pop resulted in status: ${status}, Value of x: ${x}')
status = ch.try_pop(mut y)
println('try pop resulted in status: ${status}, Value of y: ${y}')
}Close
In plain words: Calling ch.close() (often inside a defer block) signals to all listening workers: 'No more items will be sent on this channelโyou can finish up when it's empty!'
What happens in this example:
- Code note: push using arrow operator: <-
- Code note: try_push will result .closed
- Code note: We still have one more element to pop
module main
fn main() {
ch := chan int{cap: 2}
// push using arrow operator: <-
ch <- 123 // Push 1st element into the channel
ch <- 222 // Push 2nd element into the channel
println(<-ch) // pop using: <- First in is the first to out. So prints 123
ch.close() // Close channel
// try_push will result .closed
new_val := 999
status := ch.try_push(new_val)
println('try_push on a closed channel resulted in status: ${status}')
// We still have one more element to pop
println(<-ch) // 222
}Defer Close
In plain words: Calling ch.close() (often inside a defer block) signals to all listening workers: 'No more items will be sent on this channelโyou can finish up when it's empty!'
What happens in this example:
- Code note: push using arrow operator: <-
- Code note: try_push will result .closed
- Code note: We still have one more element to pop
module main
fn main() {
ch := chan int{cap: 2}
defer {
ch.close()
} // Deferred execution to Close channel
// push using arrow operator: <-
ch <- 123 // Push 1st element into the channel
ch <- 222 // Push 2nd element into the channel
println(<-ch) // pop using: <- First in is the first to out. So prints 123
// try_push will result .closed
new_val := 999
status := ch.try_push(new_val)
println('try_push on a closed channel resulted in status: ${status}')
// We still have one more element to pop
println(<-ch) // 222
}Blocking Channels
In plain words: This concurrency lesson (Blocking Channels) demonstrates how background workers (spawn) coordinate safely using channels so threads never get stuck waiting forever (deadlock).
What happens in this example:
- Uses V concurrency (
spawn/chan) to run or coordinate work safely across threads. - Prints the result to the terminal window so you can see the output right away.
module main
fn main() {
ch := chan int{}
defer {
ch.close()
}
ch <- 3
x := <-ch
println(x)
println('End main')
}Dealing Before
In plain words: This concurrency lesson (Dealing Before) demonstrates how background workers (spawn) coordinate safely using channels so threads never get stuck waiting forever (deadlock).
What happens in this example:
- Creates helper function(s)/method(s):
receiver. - Uses V concurrency (
spawn/chan) to run or coordinate work safely across threads. - Prints the result to the terminal window so you can see the output right away.
module main
fn receiver(ch chan int) {
println('Received value from the channel ${<-ch}')
}
fn main() {
ch := chan int{}
defer {
ch.close()
}
go receiver(ch)
ch <- 3
println('End main')
}Dealing After
In plain words: This concurrency lesson (Dealing After) demonstrates how background workers (spawn) coordinate safely using channels so threads never get stuck waiting forever (deadlock).
What happens in this example:
- Creates helper function(s)/method(s):
receiver. - Uses V concurrency (
spawn/chan) to run or coordinate work safely across threads. - Prints the result to the terminal window so you can see the output right away.
module main
fn receiver(ch chan int) {
println('Received value from the channel ${<-ch}')
}
fn main() {
ch := chan int{}
defer {
ch.close()
}
t := go receiver(ch)
ch <- 3
t.wait()
println('End main')
}Unbuffered Sync Before (sync_before.v)
In plain words: This concurrency lesson (Unbuffered Sync Before) demonstrates how background workers (spawn) coordinate safely using channels so threads never get stuck waiting forever (deadlock).
What happens in this example:
- Creates helper function(s)/method(s):
sender, receiver. - Uses V concurrency (
spawn/chan) to run or coordinate work safely across threads. - Prints the result to the terminal window so you can see the output right away.
module main
const count = 4
fn sender(ch chan int) {
for i in 0 .. count {
ch <- i // since the push operation is a void expression, this cannot be placed in a println
println('Sent ${i} into the channel')
}
}
fn receiver(ch chan int) {
println('Received value from the channel ${<-ch}')
}
fn main() {
ch := chan int{}
defer {
ch.close()
}
t := go receiver(ch)
go sender(ch)
t.wait()
println('End main')
}Unbuffered Sync After (sync_after.v)
In plain words: This concurrency lesson (Unbuffered Sync After) demonstrates how background workers (spawn) coordinate safely using channels so threads never get stuck waiting forever (deadlock).
What happens in this example:
- Creates helper function(s)/method(s):
sender, receiver. - Uses V concurrency (
spawn/chan) to run or coordinate work safely across threads. - Prints the result to the terminal window so you can see the output right away.
module main
const count = 4
fn sender(ch chan int) {
for i in 0 .. count {
ch <- i // since the push operation is a void expression, this cannot be placed in a println
println('Sent ${i} into the channel')
}
}
fn receiver(ch chan int) {
for _ in 0 .. count {
println('Received value from the channel ${<-ch}')
}
}
fn main() {
ch := chan int{}
defer {
ch.close()
}
t := go receiver(ch)
go sender(ch)
t.wait()
println('End main')
}Understanding Buffered Channel (buffered_channel.v)
In plain words: This concurrency lesson (Understanding Buffered Channel) demonstrates how background workers (spawn) coordinate safely using channels so threads never get stuck waiting forever (deadlock).
What happens in this example:
- Uses V concurrency (
spawn/chan) to run or coordinate work safely across threads. - Prints the result to the terminal window so you can see the output right away.
module main
fn main() {
ch := chan int{cap: 1}
defer {
ch.close()
}
ch <- 3
x := <-ch
println(x)
println('End main')
}Coroutines Communication
In plain words: This concurrency lesson (Coroutines Communication) demonstrates how background workers (spawn) coordinate safely using channels so threads never get stuck waiting forever (deadlock).
What happens in this example:
- Creates helper function(s)/method(s):
sender, receiver. - Uses V concurrency (
spawn/chan) to run or coordinate work safely across threads. - Prints the result to the terminal window so you can see the output right away.
module main
fn sender(ch chan int) {
val := 3
println('Sending value: ${val} in the channel')
ch <- val
println('sent value: ${val} in the channel')
}
fn receiver(ch chan int) {
println('Received value from the channel ${<-ch}')
}
fn main() {
ch := chan int{cap: 1}
defer {
ch.close()
}
t := go receiver(ch)
go sender(ch)
t.wait()
println('End main')
}Buffered Sync Before (sync_before.v)
In plain words: This concurrency lesson (Buffered Sync Before) demonstrates how background workers (spawn) coordinate safely using channels so threads never get stuck waiting forever (deadlock).
What happens in this example:
- Creates helper function(s)/method(s):
sender, receiver. - Uses V concurrency (
spawn/chan) to run or coordinate work safely across threads. - Prints the result to the terminal window so you can see the output right away.
module main
const count = 4
fn sender(ch chan int) {
for i in 0 .. count {
ch <- i
println('sent value: ${i} in the channel')
}
}
fn receiver(ch chan int) {
println('Received value from the channel ${<-ch}')
}
fn main() {
ch := chan int{cap: 2}
defer {
ch.close()
}
t := go receiver(ch)
go sender(ch)
t.wait()
println('End main')
}Buffered Sync After (sync_after.v)
In plain words: This concurrency lesson (Buffered Sync After) demonstrates how background workers (spawn) coordinate safely using channels so threads never get stuck waiting forever (deadlock).
What happens in this example:
- Creates helper function(s)/method(s):
sender, receiver. - Uses V concurrency (
spawn/chan) to run or coordinate work safely across threads. - Prints the result to the terminal window so you can see the output right away.
module main
const count = 4
fn sender(ch chan int) {
for i in 0 .. count {
ch <- i
println('sent value: ${i} in the channel')
}
}
fn receiver(ch chan int) {
for _ in 0 .. count {
println('Received value from the channel ${<-ch}')
}
}
fn main() {
ch := chan int{cap: 2}
defer {
ch.close()
}
t := go receiver(ch)
go sender(ch)
t.wait()
println('End main')
}Channel Select Before
In plain words: A select { ... } block lets your program watch multiple channels at the same time and react to whichever channel sends or receives a message first!
What happens in this example:
- Creates helper function(s)/method(s):
process1, process2. - Uses V concurrency (
spawn/chan) to run or coordinate work safely across threads. - Prints the result to the terminal window so you can see the output right away.
module main
fn process1(ch chan int) {
for i in 1 .. 6 {
sq := i * i
println('process1: value being pushed on ch1: ${sq}')
ch <- sq
}
}
fn process2(ch chan string) {
msg := 'hello from process 2'
println('process2: value being pushed on ch2: ${msg}')
ch <- msg
}
fn main() {
ch1 := chan int{cap: 5} // buffered channel
ch2 := chan string{} // unbuffered channel
defer {
ch1.close()
ch2.close()
}
go process1(ch1)
go process2(ch2)
select {
a := <-ch1 {
println('main: value popped from ch1: ${a}')
}
b := <-ch2 {
println('main: value popped from ch2: ${b}')
}
}
}Channel Select
In plain words: A select { ... } block lets your program watch multiple channels at the same time and react to whichever channel sends or receives a message first!
What happens in this example:
- Code note: this case executes for every 2 seconds of inactivity by any other channels in this select statement
module main
import time
fn process1(ch chan int) {
for i in 1 .. 6 {
sq := i * i
time.sleep(3 * time.second)
println('process1: value being pushed on ch1: ${sq}')
ch <- sq
}
}
fn process2(ch chan string) {
msg := 'hello from process 2'
println('process2: value being pushed on ch2: ${msg}')
ch <- msg
}
fn main() {
ch1 := chan int{cap: 5} // buffered channel
ch2 := chan string{} // unbuffered channel
defer {
ch1.close()
ch2.close()
}
go process1(ch1)
go process2(ch2)
mut sec := 0
for {
select {
a := <-ch1 {
sec = 0
println('main: value popped from ch1: ${a}')
}
b := <-ch2 {
sec = 0
println('main: value popped from ch2: ${b}')
}
2 * time.second {
// this case executes for every 2 seconds of inactivity by any other channels in this select statement
sec = sec + 2
println('main: more than ${sec}s passed without a channel being ready')
if sec >= 6 {
println('exiting out of select after ${sec} seconds of inactivity amongst channels')
break
}
}
}
}
println('done')
}Deep Dive Explanation: Advanced Channel Operations & Multiplexing
1. Non-Blocking Senders & Receivers (`try_push` and `try_pop`)
If your program cannot afford to block (e.g., in high-frequency game loops or real-time networking threads), V provides non-blocking channel API methods:
try_push(val): Attempts to sendvalimmediately. It returns aChanStatusenum:.success: The value was written to the channel's buffer..not_ready: The operation would block (either because the channel is unbuffered with no active reader, or the buffered channel is full)..closed: The channel was closed, and writing is invalid.try_pop(&mut_var): Attempts to read immediately. It takes a reference to a mutable variable where the received value will be stored and returns aChanStatusenum.
2. Channel Multiplexing with `select`
V's select statement monitors multiple channel operations simultaneously:
- Blocking Mode: The
selectblock will halt execution until at least one of the specified channel reads or writes is ready. When a case is ready, its block executes, and control leaves theselectblock. - Inactivity Timeouts: You can declare a timeout block (e.g.,
2 * time.second { ... }). If all other monitored channels remain inactive for that duration, this case triggers. This is highly useful for implementing idle detection or connection timeouts. - Non-Blocking Mode (
else): Adding anelse { ... }block to aselectstatement makes it entirely non-blocking. If no channels are immediately ready, theelseblock executes instantly.
V-Routines & Concurrency
Stopwatch Demo
In plain words: V's time.new_stopwatch() acts like a digital stopwatch you can .start(), .pause(), and check with .elapsed() to measure how long a piece of code takes to run.
What happens in this example:
- Prints the result to the terminal window so you can see the output right away.
module main
import time
fn main() {
sw := time.new_stopwatch()
for i in 1 .. 5 {
println('${i}')
}
println('Total time took to finish: ${sw.elapsed().seconds()} seconds')
}Spawn Void Function
In plain words: This concurrency lesson (Spawn Void Function) shows how spawn launches functions in the background and how .wait() collects their finished results.
What happens in this example:
- Creates helper function(s)/method(s):
greet. - Prints the result to the terminal window so you can see the output right away.
module main
fn greet() {
println('Hello from other side!')
}
fn main() {
h := go greet()
println(typeof(h).name) // thread
}Waiting On Concurrent Thread
In plain words: This concurrency lesson (Waiting On Concurrent Thread) shows how spawn launches functions in the background and how .wait() collects their finished results.
What happens in this example:
- Creates helper function(s)/method(s):
greet. - Prints the result to the terminal window so you can see the output right away.
module main
fn greet() {
println('Hello from other side!')
}
fn main() {
h := go greet()
println(typeof(h).name)
h.wait()
}Running Multiple Tasks In Sequence
In plain words: This lesson (Running Multiple Tasks In Sequence) shows a clear, runnable V example from concurrency/03_concurrency_real_life_scenario/01_running_multiple_tasks_in_sequence/01_running_multiple_tasks_in_sequence.v that you can test directly in your terminal or the V Playground.
What happens in this example:
- Creates helper function(s)/method(s):
hot_water, brush_teeth, select_clothes. - Prints the result to the terminal window so you can see the output right away.
module main
import time
fn hot_water() {
println('Started Switch on Water heater: ${time.now().hhmmss()}')
time.sleep(5 * time.second)
println('Water heater indicates hot water ready!: ${time.now().hhmmss()}')
}
fn brush_teeth() {
println('Started brushing: ${time.now().hhmmss()}')
time.sleep(3 * time.second)
println('End Brushing: ${time.now().hhmmss()}')
}
fn select_clothes() {
println('Started choosing pair of clothes : ${time.now().hhmmss()}')
time.sleep(3 * time.second)
println('End choosing pair of clothes: ${time.now().hhmmss()}')
}
fn main() {
sw := time.new_stopwatch()
hot_water()
brush_teeth()
select_clothes()
println('Your pre bath morning chores took: ${sw.elapsed().seconds()} seconds')
}Spawning Multiple Tasks Concurrently
In plain words: This concurrency lesson (Spawning Multiple Tasks Concurrently) shows how spawn launches functions in the background and how .wait() collects their finished results.
What happens in this example:
- Creates helper function(s)/method(s):
hot_water, brush_teeth, select_clothes. - Uses
mutso the variable or collection is allowed to be updated after creation. - Prints the result to the terminal window so you can see the output right away.
module main
import time
fn hot_water() {
println('Started Switch on Water heater: ${time.now().hhmmss()}')
time.sleep(5 * time.second)
println('Water heater indicates hot water ready! : ${time.now().hhmmss()}')
}
fn brush_teeth() {
println('Started brushing: ${time.now().hhmmss()}')
time.sleep(3 * time.second)
println('End Brushing: ${time.now().hhmmss()}')
}
fn select_clothes() {
println('Started choosing pair of clothes: ${time.now().hhmmss()}')
time.sleep(3 * time.second)
println('End choosing pair of clothes: ${time.now().hhmmss()}')
}
fn main() {
mut t := []thread{}
sw := time.new_stopwatch()
t << go hot_water()
t << go brush_teeth()
t << go select_clothes()
t.wait()
println('Your pre bath morning chores took: ${sw.elapsed().seconds()} seconds')
}Functions With Return Values
In plain words: This concurrency lesson (Functions With Return Values) shows how spawn launches functions in the background and how .wait() collects their finished results.
What happens in this example:
- Creates helper function(s)/method(s):
hot_water, brush_teeth, select_clothes. - Uses
mutso the variable or collection is allowed to be updated after creation. - Prints the result to the terminal window so you can see the output right away.
module main
import time
fn hot_water() string {
println('Started Switch on Water heater: ${time.now().hhmmss()}')
time.sleep(5 * time.second)
println('Water heater indicates hot water ready! : ${time.now().hhmmss()}')
return 'Hot water ready!'
}
fn brush_teeth() string {
println('Started brushing: ${time.now().hhmmss()}')
time.sleep(3 * time.second)
println('End Brushing: ${time.now().hhmmss()}')
return 'Sparkling Teeth ready!'
}
fn select_clothes() string {
println('Started choosing pair of clothes: ${time.now().hhmmss()}')
time.sleep(3 * time.second)
println('End choosing pair of clothes: ${time.now().hhmmss()}')
return 'Pair of clothes ready!'
}
fn main() {
mut t := []thread string{}
sw := time.new_stopwatch()
t << go hot_water()
t << go brush_teeth()
t << go select_clothes()
res := t.wait()
println('Your pre bath morning chores took: ${sw.elapsed().seconds()} seconds')
println('*** Type Check ***')
println('Type of thread array of strings t: ${typeof(t).name}')
println('Type of res: ${typeof(res).name}')
println('*** Values returned by concurrently executed tasks ***')
println(res)
}Spawn Anonymous Funcs Without Input Args
In plain words: This concurrency lesson (Spawn Anonymous Funcs Without Input Args) shows how spawn launches functions in the background and how .wait() collects their finished results.
What happens in this example:
- Prints the result to the terminal window so you can see the output right away.
module main
fn main() {
t := go fn () string {
return 'hi'
}()
x := t.wait()
println(typeof(x).name) // string
println(x) // hi
}Spawn Anonymous Funcs With Input Args
In plain words: This concurrency lesson (Spawn Anonymous Funcs With Input Args) shows how spawn launches functions in the background and how .wait() collects their finished results.
What happens in this example:
- Creates helper function(s)/method(s):
string. - Uses
mutso the variable or collection is allowed to be updated after creation. - Prints the result to the terminal window so you can see the output right away.
module main
fn main() {
mut t := []thread string{}
for i in 1 .. 3 {
t << go fn (i int, msg string) string {
return 'iteration: ${i}, message: ${msg}'
}(i, 'hello') // <- arguments must match list in the anonymous function definition
}
res := t.wait()
println('Type of t: ${typeof(t).name}')
println('Type of res: ${typeof(res).name}')
println(res)
}Sharing Data Main And Concurrent Tasks
In plain words: Marking a variable as shared and wrapping reads/writes in rlock or lock { ... } lets multiple background threads share and update the same data safely.
In addition to channels, V supports shared-memory concurrency using the shared keyword. Multiple threads can safely read and write to the same struct using lock (exclusive write lock) and rlock (shared read lock) blocks. This prevents race conditions and ensures synchronization without manual mutex management.
module main
import rand
// 1. Define a shared struct type.
// Structs that are intended to be shared across multiple threads should be defined normally.
// Mutability of fields is indicated as usual (e.g., total and num_donors under mut:).
struct Fund {
name string
target f32
mut:
total f32
num_donors int
}
// 2. Define a method on a shared receiver.
// In V, a receiver can be marked as 'shared' to indicate that the struct instance
// passed to it will be accessed concurrently.
fn (shared f Fund) collect(amt f32) {
// 3. Acquire a write lock.
// The `lock` block ensures exclusive (read-write) access to the shared object 'f'.
// Only one thread can execute within the lock block at a time. Other threads attempting
// to lock 'f' will block until this block exits.
lock f {
if f.total < f.target {
f.num_donors += 1
f.total += amt
// We can safely read and write to the struct fields inside the lock block.
println('${f.num_donors} \t before: ${f.total - amt} \t funds received: ${amt} \t total: ${f.total}')
}
}
}
// donation simulates generating a random donation amount.
fn donation() f32 {
// rand.f32_in_range returns a result/option type, so we use `or` to handle default.
return rand.f32_in_range(100.00, 250.00) or { 100.00 }
}
fn main() {
// 4. Declare a shared variable.
// The `shared` keyword before the variable name makes it a shared object.
// Under the hood, V automatically associates a mutex with this object.
shared fund := Fund{
name: 'A noble cause'
target: 1000.00
}
for {
// 5. Acquire a read lock (rlock).
// A read lock allows multiple threads to read the shared object concurrently
// but prevents any thread from writing to it.
rlock fund {
if fund.total >= fund.target {
break
}
}
// 6. Spawn concurrent tasks.
// The `go` keyword (interchangeable with `spawn`) starts a function in a new thread.
// `go donation()` returns a thread handle `h`.
h := go donation()
// Spawning `fund.collect` concurrently and passing the result of `h.wait()`.
// `h.wait()` blocks the main loop until the `donation()` thread finishes and returns its f32 value.
go fund.collect(h.wait())
}
// 7. Final output with read lock.
rlock fund {
println('${fund.num_donors} donors donated for ${fund.name}')
println('${fund.name} raised total fund amount: \$ ${fund.total}')
}
}Deep Dive Explanation: V-Routines & Shared Memory Concurrency
1. Coroutines (V-Routines) via `spawn` and `go`
V supports lightweight concurrency using v-routines, which are spawned using the spawn keyword (the go keyword acts as an alias).
- When you call
spawn task(), V runs the function concurrently. - V's runtime schedules these v-routines across an OS thread pool, making them highly efficient and lightweight.
2. Thread Handles & Blocking on `.wait()`
Every spawn operation returns a thread handle:
- If the function returns a value of type
T, the handle has the typethread T. - If the function does not return a value (void), the handle is of type
thread. - Calling
.wait()on a thread handle (e.g.,result := handle.wait()) blocks the calling thread until the spawned routine completes, retrieving its return value. - You can manage multiple threads by pushing their handles into an array and waiting on all of them at once:
mut threads := []thread int{}
threads << spawn worker(1)
threads << spawn worker(2)
results := threads.wait() // Returns []int containing results from all workers3. Shared State Mutexes (`shared`)
For shared-memory concurrency, V does not allow raw, unsynchronized access to global or heap variables across threads. Instead, variables must be explicitly marked as shared:
shared fund := Fund{...}instructs the compiler to automatically associate a mutex with thefundinstance.- Struct methods can accept a shared receiver (e.g.,
fn (shared f Fund) collect(...)).
4. Safe Synchronization: `lock` and `rlock` Blocks
To prevent data races, V enforces a strict compile-time lock check. You cannot access or modify a shared variable's fields directly. You must wrap the access in a lock block:
lock variable { ... }: Acquires an exclusive read-write lock. Use this block whenever you mutate fields of the shared structure. Only one thread can hold this lock at a time.rlock variable { ... }: Acquires a shared read-only lock. Multiple threads can enter anrlockblock concurrently to read fields, but any thread attempting to acquire alockwill be blocked until all readers exit.
Chapter 12 Working with Databases and JSON
Plain-English Chapter Guide (Start Here if You Are New to Coding!)
- The Big Idea in Simple Words: Almost every real app needs to save data so it isn't lost when the program closes, and talk to other apps over the internet. This chapter teaches the two most popular ways to do that:
- JSON is a universal text format that websites and apps use to send data back and forth (like a digital postcard). V can turn any struct into JSON (
encode) or turn JSON back into a struct (decode) in one line! - SQLite & ORM let you store data in a real database table (like a super-fast spreadsheet file on disk). V's built-in ORM lets you query database tables using clean V code (
sql db { select from User }) without having to write raw database strings by hand!
- Words to Know:
- JSON: Text formatted as
{"name": "Alice", "age": 30}used by almost all web APIs and config files. - Encode / Serialize: Turning a V struct into a JSON text string.
- Decode / Deserialize: Turning a JSON text string back into a V struct.
- SQLite: A lightweight database stored in a single file (or in memory with
':memory:'). - CRUD: Short for Create, Read, Update, and Deleteโthe four basic things you do with saved data.
Quick Access โ Jump to any lesson in this chapter
Below is an index of all code examples in this chapter. You can use these links to jump directly to any specific code example:
Case Study: Notes API
JSON & ORM
- Decode
- Encode
- Json To From File
- Json Array Of Objects
- Json Map To From File
- Json Array To From File
- Orm Demo
SQLite Integration
Sqlite Raw Crud
Most applications need to work with databases or API payloads. This chapter teaches you how to serialize and deserialize JSON data, use V's built-in ORM with SQLite, and covers a complete Notes REST API case study.
Under the Hood: Compile-Time Reflection & Type-Safe ORM
๐ง Optional Deep Dive โ Click to Expand / Collapse
Plain-English Summary: In simple words: V writes custom JSON and database code for your structs at the moment you compile your app, which means converting JSON and querying SQLite databases is both fast and typo-proof.
Working with external data formats (JSON) and databases (SQLite, PostgreSQL, MySQL) in V is uniquely fast and safe thanks to compile-time code generation.
Zero Runtime Reflection in JSON Serialization
In languages like Go, Java, or Python, JSON decoding relies on runtime reflection (reflect). Dynamic type inspection on every incoming request consumes significant CPU cycles and degrades API throughput.
In V:
user := json2.decode[User](payload)!* Compile-Time Generation: During compilation, the V compiler inspects the definition of User and generates a dedicated, custom C parsing function tailored specifically to that struct.
* Zero Reflection Overhead: When json2.decode executes at runtime, it calls this pre-compiled C parser directly, resulting in speeds comparable to handcrafted low-level serializers.
* Struct Attributes: @[json: 'custom_name'] customizes JSON field names, while @[skip] excludes sensitive fields from serialization.
Compile-Time Type-Safe V ORM (`sql db { ... }`)
V includes a native Object-Relational Mapping (ORM) engine embedded directly into the compiler syntax:
users := sql db {
select from User where age >= 18 && status == 'active' order by name limit 10
}Under the hood:
- Compile-Time Validation: The V compiler parses the SQL query block and verifies table names, column names, and comparison types against the
Userstruct definition. A typo in a column name produces a compile-time error, not a runtime crash! - Automatic Parameterization: The compiler transforms V variables (e.g.
'active') into parameterized SQL prepared statement placeholders (?), completely eliminating SQL injection vulnerabilities. - Automatic Hydration: Results returned from the database driver are mapped directly into instances of
Userwithout manual column-by-column unpacking.
Case Study: Notes API
Notes API Case Study - Main (main.v)
In plain words: Part of the complete Notes Web API case study (Notes API Case Study - Main), showing how a real V web server stores and serves notes using structs, JSON, and SQLite.
What happens in this example:
- Defines custom data blueprint(s):
App, Context. - Uses
mutso the variable or collection is allowed to be updated after creation. - Includes an
or { ... }safety block to handle potential errors or missing values calmly.
module main
import veb
import db.sqlite
struct App {
mut:
db sqlite.DB
}
struct Context {
veb.Context
}
fn main() {
mut db := sqlite.connect('notes.db') or { panic(err) }
defer {
db.close() or {}
}
db.exec('drop table if exists Notes') or { panic(err) }
sql db {
create table Note
} or { panic(err) }
http_port := 8000
mut app := &App{
db: db
}
veb.run[App, Context](mut app, http_port)
}Note
In plain words: Part of the complete Notes Web API case study (Note), showing how a real V web server stores and serves notes using structs, JSON, and SQLite.
What happens in this example:
- Code note: malformed json
- Code note: before we save, we must ensure the note's message is unique
- Code note: save to db
- Code note: retrieve the last id from the db to build full Note object
module main
import json2
import veb
@[table: 'Notes']
struct Note {
id int @[primary; sql: serial]
message string @[sql: 'detail'; unique]
status bool
}
@['/notes'; post]
fn (mut app App) create(mut ctx Context) veb.Result {
// malformed json
n := json2.decode[Note](ctx.req.data) or {
ctx.res.set_status(.bad_request)
return ctx.json(error_response(400, invalid_json))
}
// before we save, we must ensure the note's message is unique
notes_found := sql app.db {
select from Note where message == n.message
} or {
ctx.res.set_status(.internal_server_error)
return ctx.json(error_response(500, err.msg()))
}
if notes_found.len > 0 {
ctx.res.set_status(.bad_request)
return ctx.json(error_response(400, unique_message))
}
// save to db
sql app.db {
insert n into Note
} or {
ctx.res.set_status(.internal_server_error)
return ctx.json(error_response(500, err.msg()))
}
// retrieve the last id from the db to build full Note object
new_id := app.db.last_id() as int
// build new note object including the new_id and send it as JSON response
note_created := Note{new_id, n.message, n.status}
ctx.res.set_status(.created)
ctx.res.header.add(.content_location, '/notes/${new_id}')
return ctx.json(json2.encode(note_created))
}
@['/notes/:id'; get]
fn (mut app App) read(mut ctx Context, id int) veb.Result {
n := sql app.db {
select from Note where id == id
} or {
ctx.res.set_status(.internal_server_error)
return ctx.json(error_response(500, err.msg()))
}
// check if note exists
if n.len == 0 {
ctx.res.set_status(.not_found)
return ctx.json(error_response(400, note_not_found))
}
// found note, return it
ret := json2.encode(n[0])
ctx.res.set_status(.ok)
return ctx.json(ret)
}
@['/notes'; get]
fn (mut app App) read_all(mut ctx Context) veb.Result {
n := sql app.db {
select from Note
} or {
ctx.res.set_status(.internal_server_error)
return ctx.json(error_response(500, err.msg()))
}
ret := json2.encode(n)
ctx.res.set_status(.ok)
return ctx.json(ret)
}
@['/notes/:id'; put]
fn (mut app App) update(mut ctx Context, id int) veb.Result {
// malformed json
n := json2.decode[Note](ctx.req.data) or {
ctx.res.set_status(.bad_request)
return ctx.json(error_response(400, invalid_json))
}
// check if note to be updated exists
note_to_update := sql app.db {
select from Note where id == id
} or {
ctx.res.set_status(.internal_server_error)
return ctx.json(error_response(500, err.msg()))
}
if note_to_update.len == 0 {
ctx.res.set_status(.not_found)
return ctx.json(error_response(404, note_not_found))
}
// before update, we must ensure the note's message is unique
// id != id for idempotency
// message == n.message for unique check
res := sql app.db {
select from Note where message == n.message && id != id
} or {
ctx.res.set_status(.internal_server_error)
return ctx.json(error_response(500, err.msg()))
}
if res.len > 0 {
ctx.res.set_status(.bad_request)
return ctx.json(error_response(400, unique_message))
}
// update the note
sql app.db {
update Note set message = n.message, status = n.status where id == id
} or {
ctx.res.set_status(.internal_server_error)
return ctx.json(error_response(500, err.msg()))
}
// build the updated note using the :id and request body
// instead of making one more db call
updated_note := Note{id, n.message, n.status}
ret := json2.encode(updated_note)
ctx.res.set_status(.ok)
return ctx.json(ret)
}
@['/notes/:id'; delete]
fn (mut app App) delete(mut ctx Context, id int) veb.Result {
sql app.db {
delete from Note where id == id
} or {
ctx.res.set_status(.internal_server_error)
return ctx.json(error_response(500, err.msg()))
}
ctx.res.set_status(.no_content)
return ctx.ok('')
}Util
In plain words: Part of the complete Notes Web API case study (Util), showing how a real V web server stores and serves notes using structs, JSON, and SQLite.
What happens in this example:
- Defines custom data blueprint(s):
NotesResponse. - Creates helper function(s)/method(s):
error_response.
module main
import json2
struct NotesResponse {
status int
message string
}
const invalid_json = 'Invalid JSON Payload'
const note_not_found = 'Note not found'
const unique_message = 'Please provide a unique message for Note'
fn error_response(status int, message string) string {
er := NotesResponse{status, message}
return json2.encode(er)
}JSON & ORM
Decode
In plain words: Decoding (json.decode / json2.decode) takes a raw JSON text string (like '{"name":"Alice"}') and turns it into a strongly-typed V struct you can use in your code.
What happens in this example:
- Code note: Decode a JSON payload into a struct instance.
- Code note: Print the type name and the decoded data for inspection.
import json2
struct Note {
id int
message string
status bool
}
fn main() {
// Decode a JSON payload into a struct instance.
n := json2.decode[Note]('{"id":1,"message":"Plan a holiday","status":false}') or {
panic('invalid json data')
}
// Print the type name and the decoded data for inspection.
println(typeof(n).name) // Note
println(n)
}Encode
In plain words: Encoding (json.encode / json2.encode) takes a V struct or array and turns it into a clean JSON text string ready to save to a file or send over the internet.
What happens in this example:
- Code note: Create a note object that will be converted to JSON.
- Code note: Encode the struct to a compact JSON string.
- Code note: Encode the same object with pretty formatting for readability.
import json2
struct Note {
id int
message string
status bool
}
fn main() {
// Create a note object that will be converted to JSON.
m := Note{
id: 2
message: 'Get groceries'
status: false
}
// Encode the struct to a compact JSON string.
mut j := json2.encode(m)
println(j)
// Encode the same object with pretty formatting for readability.
j = json2.encode(m, prettify: true)
println(j)
}Deep Dive Explanation: JSON Serialization & Deserialization
1. Compile-Time JSON Parsing
Unlike many languages that rely on slow, runtime reflection to inspect structures, V's compiler generates encoding and decoding code statically at compile time. This ensures extremely fast performance and safety.
2. Decoding JSON (`json2.decode`)
- To decode a JSON string, invoke
json2.decode[StructName](json_string). - Result Type Return: Since incoming JSON strings can be malformed,
json2.decodereturns a Result type (!StructName). You must unwrap it with anorblock:
user := json2.decode[User](raw_json) or {
println('Failed to parse user JSON: ${err}')
return
}3. Encoding to JSON (`json2.encode`)
- To serialize a V struct instance into a JSON string, invoke
json2.encode(instance). - This operation is guaranteed to succeed and returns a standard
stringdirectly (noorblock required).
4. Struct JSON Attribute Tags
V provides structural attributes to customize JSON mapping. These are written inside @[...] brackets placed on the same line as the field:
- Custom Naming:
field string @[json: 'custom_name']maps the struct field to the'custom_name'JSON key. - Skip Fields:
secret string @[json: '-']prevents the field from being serialized or deserialized. - Required Keys:
id int @[required]ensures that if theidkey is missing in the JSON payload, the decoder returns an error.
Json To From File
In plain words: Shows the everyday workflow of turning a V struct into JSON (json.encode), saving it to a .json file on disk (os.write_file), and loading it back (os.read_file + json.decode).
This example demonstrates how to encode an object to JSON, write it to a file, read it back, and decode it into a V struct.
What happens in this example:
- Code note: Create an object instance
- Code note: 1. Encode object to JSON string
- Code note: 2. Write JSON string to file
- Code note: 3. Read JSON string from file
module main
import json2
import os
struct Book {
title string
author string
year int
}
fn main() {
file_path := 'book.json'
// Create an object instance
book := Book{
title: 'The V Programming Language'
author: 'Alex Medvednikov'
year: 2019
}
// 1. Encode object to JSON string
println('Encoding object to JSON...')
json_str := json2.encode(book)
println('JSON string: ${json_str}')
// 2. Write JSON string to file
println('Writing JSON to file "${file_path}"...')
os.write_file(file_path, json_str) or {
eprintln('Failed to write file: ${err}')
return
}
// 3. Read JSON string from file
println('Reading JSON from file "${file_path}"...')
content := os.read_file(file_path) or {
eprintln('Failed to read file: ${err}')
return
}
// 4. Decode JSON string back to Book object
println('Decoding JSON back to object...')
decoded_book := json2.decode[Book](content) or {
eprintln('Failed to decode JSON: ${err}')
return
}
println('Decoded book: Title: "${decoded_book.title}", Author: "${decoded_book.author}", Year: ${decoded_book.year}')
// Clean up created file
os.rm(file_path) or {}
}Json Array Of Objects
In plain words: You can encode and decode an entire list of structs ([]User) to and from a JSON array ([...]) in a single call.
This example demonstrates how to serialize and deserialize an array of objects (structs) to and from JSON, and how to write/read them using the filesystem.
What happens in this example:
- Code note: Create an array of objects
- Code note: 1. Encode array of objects to JSON string
- Code note: 2. Write JSON string to file
- Code note: 3. Read JSON string from file
module main
import json2
import os
struct Task {
id int
title string
done bool
}
fn main() {
file_path := 'tasks.json'
// Create an array of objects
tasks := [
Task{
id: 1
title: 'Read V Guide'
done: false
},
Task{
id: 2
title: 'Write JSON helper examples'
done: true
},
]
// 1. Encode array of objects to JSON string
println('Encoding array of objects to JSON...')
json_str := json2.encode(tasks)
println('JSON string:\n${json_str}')
// 2. Write JSON string to file
println('\nWriting JSON array to file "${file_path}"...')
os.write_file(file_path, json_str) or {
eprintln('Failed to write file: ${err}')
return
}
// 3. Read JSON string from file
println('Reading JSON from file "${file_path}"...')
content := os.read_file(file_path) or {
eprintln('Failed to read file: ${err}')
return
}
// 4. Decode JSON string back to an array of Task objects
println('Decoding JSON back to array of objects...')
decoded_tasks := json2.decode[[]Task](content) or {
eprintln('Failed to decode JSON: ${err}')
return
}
println('Decoded array of tasks successfully!')
for task in decoded_tasks {
println(' - Task #${task.id}: "${task.title}" [Done: ${task.done}]')
}
// Clean up created file
os.rm(file_path) or {}
}Json Map To From File
In plain words: Shows how to save a V map (dictionary) directly to a JSON file and read it back into a map.
This example demonstrates how to serialize a map structure (map[string]int) into a JSON string, write it to a file, read it back, and deserialize it back into a map in V.
What happens in this example:
- Code note: Create a map[string]int
- Code note: 1. Encode map to JSON string
- Code note: 2. Write JSON string to file
- Code note: 3. Read JSON string from file
module main
import json2
import os
fn main() {
file_path := 'scores.json'
// Create a map[string]int
scores := {
'Alice': 95
'Bob': 88
'Charlie': 92
}
// 1. Encode map to JSON string
println('Encoding map to JSON...')
json_str := json2.encode(scores)
println('JSON string: ${json_str}')
// 2. Write JSON string to file
println('Writing map JSON to file "${file_path}"...')
os.write_file(file_path, json_str) or {
eprintln('Failed to write file: ${err}')
return
}
// 3. Read JSON string from file
println('Reading from file "${file_path}"...')
content := os.read_file(file_path) or {
eprintln('Failed to read file: ${err}')
return
}
// 4. Decode JSON string back to map[string]int
println('Decoding JSON back to map...')
decoded_scores := json2.decode[map[string]int](content) or {
eprintln('Failed to decode map JSON: ${err}')
return
}
println('Decoded map successfully:')
for k, v in decoded_scores {
println(' - ${k}: ${v}')
}
// Clean up created file
os.rm(file_path) or {}
}Json Array To From File
In plain words: Demonstrates saving a list (array) of values to a JSON file and restoring it cleanly.
This example demonstrates two different methods for reading and writing arrays to/from files in V:
- JSON Serialization: Best for primitive numeric/boolean arrays (e.g.
[]int). - Raw Line-by-Line (Plain Text): Best for string lists (e.g.
[]string), joining with newlines on write and using V's standardos.read_lines()on read.
module main
import json2
import os
fn main() {
// We will show two ways of writing/reading arrays to/from files:
// Method 1: Using JSON serialization (great for numeric or structured arrays)
// Method 2: Using raw text line-by-line reading/writing (great for string lists)
// --- Method 1: JSON Serialization ---
println('=== Method 1: JSON Serialization ===')
json_file_path := 'numbers.json'
numbers := [10, 20, 30, 40, 50]
println('Encoding array to JSON...')
json_str := json2.encode(numbers)
println('JSON string: ${json_str}')
println('Writing JSON to file "${json_file_path}"...')
os.write_file(json_file_path, json_str) or {
eprintln('Failed to write file: ${err}')
return
}
json_content := os.read_file(json_file_path) or {
eprintln('Failed to read file: ${err}')
return
}
decoded_numbers := json2.decode[[]int](json_content) or {
eprintln('Failed to decode array JSON: ${err}')
return
}
println('Decoded array: ${decoded_numbers}')
os.rm(json_file_path) or {}
// --- Method 2: Raw Line-by-Line (Plain Text) ---
println('\n=== Method 2: Raw Line-by-Line ===')
text_file_path := 'fruits.txt'
fruits := ['Apple', 'Banana', 'Cherry', 'Date']
println('Writing array elements to text file "${text_file_path}"...')
// Join the string array with newlines to write line-by-line
fruits_content := fruits.join('\n')
os.write_file(text_file_path, fruits_content) or {
eprintln('Failed to write file: ${err}')
return
}
println('Reading lines from file "${text_file_path}" using os.read_lines()...')
// os.read_lines reads a file directly into a []string (line by line)
read_fruits := os.read_lines(text_file_path) or {
eprintln('Failed to read lines: ${err}')
return
}
println('Read string array: ${read_fruits}')
// Clean up created files
os.rm(text_file_path) or {}
}Orm Demo
In plain words: V's built-in ORM (sql db { ... }) lets you create database tables, insert rows, and search records using normal V structs and syntax instead of writing raw SQL strings.
What happens in this example:
- Code note: Establishing a connection to the database
- Code note: Creating a table
- Code note: Inserting record(s)
- Code note: Select records
module main
import db.sqlite
@[table: 'Notes']
struct Note {
id int @[primary; sql: serial]
message string @[sql: 'detail'; unique]
status bool
}
fn main() {
// Establishing a connection to the database
mut db := sqlite.connect('NotesDB.db') or { panic(err) }
defer {
db.close() or {}
}
db.exec('drop table if exists Notes') or { panic(err) }
// Creating a table
sql db {
create table Note
} or { panic(err) }
// Inserting record(s)
n1 := Note{
message: 'Get some milk'
status: false
}
n2 := Note{
message: 'Get groceries'
status: false
}
sql db {
insert n1 into Note
insert n2 into Note
} or { panic(err) }
println(db.last_id() as int)
// Select records
all_notes := sql db {
select from Note
} or { panic(err) }
println(all_notes)
println('Type of all_notes is : ${typeof(all_notes).name}')
// Select using order by clause
notes_sorted := sql db {
select from Note order by id desc
} or { panic(err) }
println(notes_sorted)
// Select using the limit clause
notes_limited := sql db {
select from Note order by id desc limit 1
} or { panic(err) }
println(notes_limited)
println('Type returned by select when limit is 1: ${typeof(notes_limited).name}')
// Select using where clause
notes_latest := sql db {
select from Note where id > 1
} or { panic(err) }
println(notes_latest)
// Update record(s)
sql db {
update Note set status = true where id == 2
} or { panic(err) }
notes_updated := sql db {
select from Note where id == 2
} or { panic(err) }
println(notes_updated)
// Delete record(s)
sql db {
delete from Note where id == 2
} or { panic(err) }
notes_leftover := sql db {
select from Note
} or { panic(err) }
println(notes_leftover)
sql db {
drop table Note
} or { panic(err) }
println('Dropped the Note table from database!')
}Deep Dive Explanation: V's Compile-Safe Database ORM
1. Compile-Time Query Safety
V features a built-in ORM (supporting SQLite, PostgreSQL, and MySQL) that integrates directly with V's type system:
- The
sqlBlock: All ORM operations are written inside a specialsql db { ... }block. - Type Safety: The compiler validates table structures, column types, and query logic during compilation. For example, trying to compare a string field to an integer inside the
whereclause will fail to compile. - SQL Injection Prevention: All variables referenced in query clauses (like
where id == id_var) are automatically treated as query parameters under the hood, making V's ORM immune to SQL injection attacks out of the box.
2. Struct Attributes for ORM Schema Design
You configure your database schema by annotating V structs with attributes:
@[table: 'name']: Customizes the table name in the database (defaults to the struct name).@[primary; sql: serial]: Configures the field as an auto-incrementing primary key.@[sql: 'col_name']: Customizes the column name in the database (defaults to the field name).@[unique]: Adds a unique constraint to the column.
3. ORM Operations
- Create Table:
sql db { create table Note } or { ... }Generates the SQL DDL statements and creates the table based on the struct fields and attributes.
- Insert Records:
sql db { insert n1 into Note } or { ... }Inserts the struct instance into the database. If successful, V updates any auto-incrementing primary keys directly on the passed struct instance.
- Query Records (
select):
Queries return a slice of structs (e.g. []Note). Standard SQL clauses are fully supported:
where: Filter records using standard V boolean operations.order by: Sort records (e.g.,order by id desc).limit: Restrict the number of returned records.offset: Skip a number of records (used for pagination).- Update Records:
sql db { update Note set status = true where id == 2 } or { ... }Updates the records matching the predicate.
- Delete Records:
sql db { delete from Note where id == 2 } or { ... }Deletes the records matching the predicate.
SQLite Integration
Sqlite
In plain words: Connects to a SQLite database and uses V's built-in sql db { ... } ORM syntax to create tables, insert struct rows, and query data with compile-time safety.
Databases and JSON handling are essential parts of backend development. A small SQLite helper module makes your application code cleaner by centralizing connection setup, schema initialization, and CRUD operations in one place.
This example shows a practical pattern you can reuse in small tools, desktop apps, and prototypes:
- connect to a database with a single helper
- enable foreign keys for safer relations
- initialize a table with a reusable setup function
- create, read, update, and delete records with safe parameterized queries
- prevent SQL injection by using
exec_param_manyinstead of string interpolation
module sqlite
import db.sqlite as dbsqlite
pub type DB = dbsqlite.DB
pub struct Note {
id int
title string
body string
}
pub fn connect(path string) !DB {
mut db := dbsqlite.connect(path)!
db.exec('PRAGMA foreign_keys = ON;') or {
return error('failed to enable foreign keys: ${err}')
}
return db
}
pub fn connect_in_memory() !DB {
return connect(':memory:')
}
pub fn init_notes_table(mut db DB) ! {
db.exec('CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, body TEXT NOT NULL);') or {
return error('failed to create notes table: ${err}')
}
}
pub fn create_note(mut db DB, title string, body string) !int {
db.exec_param_many('INSERT INTO notes (title, body) VALUES (?, ?);', [title, body]) or {
return error('failed to insert note: ${err}')
}
return db.last_id()
}
pub fn list_notes(mut db DB) ![]Note {
rows := db.exec('SELECT id, title, body FROM notes ORDER BY id;') or {
return error('failed to list notes: ${err}')
}
mut notes := []Note{}
for row in rows {
notes << Note{
id: row.vals[0].int()
title: row.vals[1]
body: row.vals[2]
}
}
return notes
}
pub fn update_note(mut db DB, id int, title string, body string) ! {
db.exec_param_many('UPDATE notes SET title = ?, body = ? WHERE id = ?;', [title, body,
id.str()]) or { return error('failed to update note: ${err}') }
}
pub fn delete_note(mut db DB, id int) ! {
db.exec_param_many('DELETE FROM notes WHERE id = ?;', [id.str()]) or {
return error('failed to delete note: ${err}')
}
}Safe database access: always pass user input as parameters using exec_param_many or exec with placeholders like ?. This prevents SQL injection and keeps your queries readable.
SQLite CRUD Helper
In plain words: Demonstrates all four database essentials (Create, Read, Update, Delete) using V's SQLite module and helper functions.
This template demonstrates a complete SQLite database workflow using raw SQL queries. It includes creating tables, clearing data, inserting records with parameterized inputs to prevent SQL injection, and fetching records into structured types.
Key concepts illustrated:
- Database Connection: Opening and closing a SQLite database using
sqlite.connect. - Schema Management: Creating tables dynamically using
db.exec. - Parameterized Queries: Preventing SQL injection by passing variables inside string arrays using
db.exec_param_many. - Result Mapping: Manually parsing returned rows into structured V structs.
- Resource Cleanup: Appending log files or temporary databases, and deleting them cleanly via
deferblocks to prevent stray files on disk.
module main
import db.sqlite
import os
struct User {
id int
name string
email string
age int
}
fn connect_db(path string) !sqlite.DB {
return sqlite.connect(path)
}
fn init_schema(mut db sqlite.DB) ! {
db.exec('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, email TEXT UNIQUE, age INTEGER);') or {
return error('Could not create table: ${err}')
}
}
fn reset_users(mut db sqlite.DB) ! {
db.exec('DELETE FROM users;') or { return error('Could not clear users: ${err}') }
}
fn insert_user(mut db sqlite.DB, name string, email string, age int) !int {
db.exec_param_many('INSERT INTO users (name, email, age) VALUES (?, ?, ?);', [
name,
email,
age.str(),
]) or { return error('Insert failed: ${err}') }
return db.last_id()
}
fn fetch_users(mut db sqlite.DB) ![]User {
rows := db.exec('SELECT id, name, email, age FROM users ORDER BY id;') or {
return error('Select failed: ${err}')
}
mut users := []User{}
for row in rows {
users << User{
id: row.vals[0].int()
name: row.vals[1]
email: row.vals[2]
age: row.vals[3].int()
}
}
return users
}
// Reusable CRUD helpers for the SQLite boilerplate example.
fn main() {
println('=== V SQLite CRUD Boilerplate ===')
db_path := 'demo.db'
defer {
if os.exists(db_path) {
os.rm(db_path) or {}
println('Cleaned up temporary database: ${db_path}')
}
}
mut db := connect_db(db_path) or {
eprintln('${err}')
return
}
defer {
db.close() or { eprint('Failed to close database: ${err}') }
}
init_schema(mut db) or {
eprintln('${err}')
return
}
reset_users(mut db) or {
eprintln('${err}')
return
}
user_id := insert_user(mut db, 'Ada', 'ada@example.com', 36) or {
eprintln('${err}')
return
}
println('Inserted user id: ${user_id}')
users := fetch_users(mut db) or {
eprintln('${err}')
return
}
for user in users {
println('User: ${user.id} ${user.name} (${user.email}, age ${user.age})')
}
}Sqlite Raw Crud
In plain words: Shows how to run raw SQL queries (CREATE TABLE, INSERT, SELECT, UPDATE, DELETE) safely with ? parameter placeholders (db.exec_param_many) so user input can never corrupt your database.
This example demonstrates how to connect to a SQLite database and execute raw SQL queries securely using parameterized queries (db.exec_param_many) to prevent SQL Injection attacks.
It illustrates:
- Connecting with
sqlite.connect. - Executing statements that do not return tabular results (like DDL/DML) via
db.exec. - Executing parameterized DML queries (INSERT, SELECT, UPDATE, DELETE) using
db.exec_param_manyand?placeholders. - Fetching result sets from
SELECTstatements into[]sqlite.Row. - Iterating and accessing row values from
row.valsby index. - Handling resource cleanup cleanly via
defer.
SQL Injection Prevention: Avoid raw string interpolation (e.g. "$name") inside raw queries. Using db.exec_param_many forces parameter binding, rendering the query secure from malicious inputs.
module main
import db.sqlite
fn main() {
// 1. Database Connection
// Connects to a SQLite database. ':memory:' creates a temporary in-memory database.
println('Connecting to database...')
mut db := sqlite.connect(':memory:') or {
println('Connection failed: ${err}')
return
}
defer {
db.close() or { println('Failed to close database: ${err}') }
println('Database connection closed.')
}
// 2. Schema Creation (DDL)
println('Creating "users" table...')
db.exec('CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, email TEXT UNIQUE, age INTEGER);') or {
println('Table creation failed: ${err}')
return
}
// 3. Create (Insert Records using Parameterized Queries)
println('\n--- CREATE: Inserting records securely ---')
// SAFE APPROACH: Use `exec_param_many` with '?' placeholders to prevent SQL Injection.
// Parameters are passed as an array of strings: []string
db.exec_param_many('INSERT INTO users (name, email, age) VALUES (?, ?, ?);', [
'Alice',
'alice@example.com',
'30',
]) or { println('Insert failed: ${err}') }
db.exec_param_many('INSERT INTO users (name, email, age) VALUES (?, ?, ?);', [
'Bob',
'bob@example.com',
'25',
]) or { println('Insert failed: ${err}') }
db.exec_param_many('INSERT INTO users (name, email, age) VALUES (?, ?, ?);', [
'Charlie',
'charlie@example.com',
'40',
]) or { println('Insert failed: ${err}') }
println('Last inserted row ID: ${db.last_id()}')
// 4. Read (Select Records using Parameterized Queries)
println('\n--- READ: Querying records securely ---')
// Querying with parameters: only retrieve users older than 20
rows := db.exec_param_many('SELECT id, name, email, age FROM users WHERE age > ?;',
['20']) or {
println('Select failed: ${err}')
[]sqlite.Row{}
}
// Iterate and extract column values by index
for row in rows {
// Each sqlite.Row has two string arrays: `vals` (values) and `names` (column names)
id := row.vals[0]
name := row.vals[1]
email := row.vals[2]
age := row.vals[3]
println('User [ID: ${id}] -> Name: ${name}, Email: ${email}, Age: ${age}')
}
// 5. Update (Modify Records using Parameterized Queries)
println("\n--- UPDATE: Modifying Bob's email and age securely ---")
db.exec_param_many('UPDATE users SET email = ?, age = ? WHERE name = ?;', [
'bob_new@example.com',
'26',
'Bob',
]) or { println('Update failed: ${err}') }
// Verify update
updated_rows := db.exec_param_many('SELECT email, age FROM users WHERE name = ?;',
['Bob']) or { []sqlite.Row{} }
if updated_rows.len > 0 {
println("Bob's new email: ${updated_rows[0].vals[0]}")
println("Bob's new age: ${updated_rows[0].vals[1]}")
}
// 6. Delete (Remove Records using Parameterized Queries)
println('\n--- DELETE: Removing Charlie securely ---')
db.exec_param_many('DELETE FROM users WHERE name = ?;', ['Charlie']) or {
println('Delete failed: ${err}')
}
// Verify delete
remaining_rows := db.exec('SELECT name FROM users;') or { []sqlite.Row{} }
print('Remaining users: ')
for row in remaining_rows {
print('${row.vals[0]} ')
}
println('')
// 7. Cleanup
println('\nDropping "users" table...')
db.exec('DROP TABLE users;') or { println('Drop table failed: ${err}') }
}Chapter 13 Standard Library & Advanced Features
Plain-English Chapter Guide (Start Here if You Are New to Coding!)
- The Big Idea in Simple Words: V comes with "batteries included"โa massive built-in toolbox called the Standard Library (
vlib) so you can work with files (os), dates and timers (time), web requests (net.http), web servers (veb), 2D graphics (gg), passwords and security (crypto), compressed files (compress), and terminal colors (term) without needing to hunt down outside libraries! This chapter also covers power tools like Generics (writing one function that works on many types), Interfaces (defining shared abilities), and Sum Types (a box that can hold one of several chosen types). - Words to Know:
- Standard Library (
vlib): The official collection of ready-to-use modules included with V. - Interface: A checklist of methods; any struct that has those methods automatically satisfies the interface.
- Sum Type (
type Item = Book | Movie): A type that can hold either aBookor aMoviesafely. - Generic (
fn print_any[T](val T)): A template function or struct where[T]stands in for whatever type you pass in.
Quick Access โ Jump to any lesson in this chapter
Below is an index of all code examples in this chapter. You can use these links to jump directly to any specific code example:
Inline Assembly & C Interop
Networking (TCP, UDP, SSL, WebSockets)
- Net Urllib
- Net Websocket
- Websocket Persistent
- Net Html
- Net Jsonrpc
- Net Jsonrpc Persistent
- Net Ssl
- Ssl Persistent
- Net Tcp
- Tcp Persistent
- Net Udp
- Udp Persistent
- Net Unix
- Unix Persistent
Other Stdlib Updates
- Options And Results
- Generics
- Interfaces
- Sum Types
- Attributes
- Compile-Time Directives
- Strings Builder
- Os Advanced Io
- Os Operations
- Os Process Pipe
- Os System Info
- Time And Stopwatch
- Http Client
- Regex Matching
- Command Line Flags
- Datatypes Collections
- Gg Graphics
- Command Line Arguments
- Math And Rand
- Crypto Asymmetric
- Crypto Entropy
- Crypto Hash
- Crypto Kdf
- Crypto Mac
- Crypto Symmetric
- Log And Crypto
- Sync Concurrency
- Encoding Formats
- Arrays Utility
- Toml
- Strconv
- Term
- Benchmark
- Clipboard
- Semver
- Maps Standard Library Module (maps.v)
- Context
- Archive Tar
- Compress Deflate
- Compress Gzip
- Compress Szip
- Compress Zlib
- Compress Zstd
- Io Fs
- Io
- Io Util
- Hash
- Bitfield
- Cli
- Veb
- Readline
- Runtime
Strings.Lorem Helper
WebAssembly Compilation
Language Updates & Low-Level Features
This chapter highlights the power of V's standard library and advanced integration features, including low-level socket networking, inline assembly, compilation to WebAssembly, and V's unique memory management models.
Under the Hood: Sum Types, Generics, Memory Models & C Interop
๐ง Optional Deep Dive โ Click to Expand / Collapse
Plain-English Summary: In simple words: V gives you high-level conveniences (like Generics, Sum Types, and automatic memory cleanup) that compile down to fast native code and can talk directly to C libraries with zero slowdown.
Chapter 13 explores advanced systems capabilities that make V capable of everything from low-level operating system programming to high-level web infrastructure.
Sum Types (Tagged Unions) & Smart Casting
A Sum Type allows a value to hold one of several distinct types:
type Shape = Circle | Rectangle | PointUnder the hood:
* V implements sum types as a C tagged union containing an integer type ID and a shared memory union for the variant payload.
* Matching on a sum type (match shape { Circle { ... } }) or using if shape is Circle performs a type check and smart-casts the variable to Circle within that scope with zero runtime conversion penalty.
Generics via Compile-Time Monomorphization
Generics allow writing type-agnostic data structures and algorithms:
fn find_first[T](items []T, predicate fn(T) bool) ?T {
for item in items {
if predicate(item) { return item }
}
return none
}Under the hood, V implements generics through monomorphization. When the compiler sees find_first[int] and find_first[string], it generates two specialized C functions (find_first_int and find_first_string). This guarantees peak CPU performance with zero virtual dispatch, dynamic boxing, or memory indirection.
V's Pluggable Memory Management Strategies
| Memory Mode | Compiler Flag | Mechanism | Ideal Use Case |
|---|---|---|---|
| Boehm GC (Default) | v -gc boehm | Conservative, multi-threaded tracing garbage collector. | General application development, CLI tools, REST APIs. |
| Autofree | v -autofree | Static lifetime analysis engine injects free() calls automatically at compile time. | High-performance services requiring deterministic memory cleanup with zero GC pauses. |
| Manual / Low-Level | unsafe { ... } | Manual allocation via malloc / free with raw pointers. | Embedded systems, OS kernels, high-frequency trading engines. |
Zero-Overhead C Interoperability & Inline Assembly
V was designed from day one for 100% transparent C interoperability:
* Direct C Header Inclusion: Use #include <sqlite3.h> and #flag -lsqlite3 to link native C libraries directly into your V project.
* Direct C API Invocations: Call C functions directly using the C. prefix (e.g. C.sqlite3_open(...)) without any Foreign Function Interface (FFI) bindings or marshaling overhead.
* Inline Assembly (#asm): Embed raw machine assembly instructions directly inside V functions for ultra-specialized hardware control.
Inline Assembly & C Interop
Inline Assembly
In plain words: For rare, ultra-low-level hardware code, V's asm block lets you write raw CPU instructions (arm64 or amd64) with a safe V fallback for other computers.
V supports inline assembly block definitions using the asm keyword, allowing developers to execute architecture-specific instructions directly from V code. It integrates directly with V variables by mapping them to inputs and outputs using register constraints.
This example demonstrates how to:
- Write inline assembly blocks targeted at specific architectures (e.g.
arm64andamd64). - Utilize V's compile-time conditional block
$ifto compile architecture-specific assembly blocks. - Map V variables to assembly inputs and outputs using semicolon constraint annotations (e.g.
; +r (res)).
module main
// add_five adds 5 to the given integer using inline assembly.
// It uses compile-time conditional checks ($if) to select the correct
// assembly instructions depending on the target CPU architecture.
fn add_five(val int) int {
mut res := val
$if arm64 {
// ARM64 inline assembly for Apple Silicon (macOS M-series) and ARM Linux/Android.
// - Syntax: add destination, operand1, operand2
// - Constraints: '; +r (res)' specifies that 'res' is both an input and output register.
asm arm64 {
add res, res, 5
; +r (res)
}
} $else $if amd64 {
// AMD64 (x86_64) inline assembly for Intel/AMD processors.
// - Syntax: add destination, source
// - Constraints: '; +r (res)' specifies that 'res' is both an input and output register.
asm amd64 {
add res, 5
; +r (res)
}
} $else {
// Fallback for other architectures (e.g. 32-bit x86, RISC-V, WebAssembly, etc.)
res += 5
}
return res
}
// multiply_by_two multiplies the given integer by 2 using bit shifting in inline assembly.
fn multiply_by_two(val int) int {
mut res := val
$if arm64 {
// ARM64 inline assembly using logical shift left (lsl) by 1 bit.
asm arm64 {
lsl res, res, 1
; +r (res)
}
} $else $if amd64 {
// AMD64 inline assembly using shift arithmetic left (sal).
asm amd64 {
sal res, 1
; +r (res)
}
} $else {
res = res << 1
}
return res
}
fn main() {
println('=== V Inline Assembly (asm) Demo ===')
// Test 1: Addition using inline assembly
num := 10
num_plus_five := add_five(num)
println('Result of add_five(${num}): ${num_plus_five}')
assert num_plus_five == 15
// Test 2: Multiplication (bit-shifting) using inline assembly
val := 21
val_double := multiply_by_two(val)
println('Result of multiply_by_two(${val}): ${val_double}')
assert val_double == 42
println('All inline assembly assertions successfully verified!')
}C Interop
In plain words: V can call standard C functions (like C.printf, C.sqrt, or C.qsort) directly with zero wrapper delayโmaking it easy to use any existing C library!
V is designed with first-class support for C integration. Since V compiles directly to C, calling C library functions, passing C structs, and compiling legacy C code alongside V is highly performant and requires no heavy wrapper generator.
This example demonstrates how to:
- Include standard C headers using
#include <header.h>. - Declare C functions using the
fn C.name(args) typesyntax. - Map C structures in V using
@[typedef] struct C.nameto represent C typedefs. - Interact with C variables, functions, and structs directly from V.
module main
// 1. Include C standard headers.
// V compiles directly to C, so we can use C preprocessor directives like `#include`
// to bring in C standard library definitions or external C headers.
#include <math.h>
#include <stdlib.h>
// 2. Declare C functions.
// We declare C functions using the `fn C.name(args) return_type` syntax.
// The V compiler translates calls to these functions directly to the native C functions.
fn C.abs(x int) int
fn C.sqrt(x f64) f64
// 3. Declare C Structs.
// V can also interact with C structs. We use `struct C.name` to define them.
// The `@[typedef]` attribute tells the V compiler that `div_t` is a typedef structure
// in C (defined in <stdlib.h>) so it does not prefix it with the `struct` keyword in the C output.
@[typedef]
struct C.div_t {
quot int
rem int
}
// Declare C.div function from stdlib.h which returns a C.div_t struct.
fn C.div(numer int, denom int) C.div_t
fn main() {
println('=== V C Interop Demo ===')
// 4. Calling C.abs
negative_val := -42
absolute_val := C.abs(negative_val)
println('C.abs(${negative_val}) = ${absolute_val}')
assert absolute_val == 42
// 5. Calling C.sqrt
float_val := 16.0
square_root := C.sqrt(float_val)
println('C.sqrt(${float_val}) = ${square_root}')
assert square_root == 4.0
// 6. Working with C Structs and functions returning C Structs
numerator := 10
denominator := 3
div_result := C.div(numerator, denominator)
println('C.div(${numerator}, ${denominator}) -> Quotient: ${div_result.quot}, Remainder: ${div_result.rem}')
assert div_result.quot == 3
assert div_result.rem == 1
println('All C Interop functions successfully executed and verified!')
}Networking (TCP, UDP, SSL, WebSockets)
Net Urllib
In plain words: The net.urllib module parses web URLs (https://example.com:8080/path?q=vlang) into their individual parts (scheme, host, path, query parameters) and safely escapes special characters.
The net.urllib standard library module provides utilities for parsing, analyzing, and constructing Uniform Resource Locators (URLs). When working with remote HTTP APIs, you frequently need to break URLs down into constituent components (like schemes, hosts, ports, paths, credentials, and parameters) or perform URL-encoding/decoding on query strings so they are transmitted safely across network interfaces. V uses the option type pattern (or { ... }) on methods like parse to ensure that malformed URLs are caught cleanly without crashing the program.
This example illustrates parsing URLs, escaping special string characters, and creating encoded query objects.
This example demonstrates parsing URLs into components, escaping and unescaping query parameters, and encoding query parameters using the net.urllib module.
module main
import net.urllib
fn main() {
println('=== net.urllib Module Demo ===')
// 1. Parsing a URL
raw_url := 'https://user:pass@vlang.io:8080/docs/stdlib?lang=v&version=0.5.1#intro'
println('Parsing URL: ${raw_url}')
u := urllib.parse(raw_url) or {
println('Failed to parse URL: ${err}')
return
}
println('Parsed URL parts:')
println(' Scheme: ${u.scheme}')
println(' Host: ${u.host}')
println(' Path: ${u.path}')
println(' Query: ${u.raw_query}')
println(' Fragment: ${u.fragment}')
// 2. Query escaping and unescaping
original_query := 'V compiler version 0.5.1 & details'
escaped := urllib.query_escape(original_query)
unescaped := urllib.query_unescape(escaped) or { 'failed' }
println('\nQuery Escaping:')
println(' Original: ${original_query}')
println(' Escaped: ${escaped}')
println(' Unescaped: ${unescaped}')
// 3. Managing Query Parameters using urllib.Values
println('\nManaging Query Values:')
mut query_params := urllib.new_values()
query_params.add('format', 'json')
query_params.add('tags', 'programming')
query_params.add('tags', 'tutorial')
query_params.set('version', '0.5.1')
// Encode to raw query string
encoded_query := query_params.encode()
println(' Encoded query string: ${encoded_query}')
// Parse it back
parsed_params := urllib.parse_query(encoded_query) or { urllib.new_values() }
println(' Parsed format tag: ${parsed_params.get('format') or { 'none' }}')
println(' Parsed tags: ${parsed_params.get_all('tags')}')
}Net Websocket
In plain words: Shows how a V client and server open a real-time WebSocket connection (net.websocket) to push instant messages back and forth.
The net.websocket module provides robust client and server APIs for real-time bidirectional communication over WebSockets. Under the hood, V's WebSocket implementation supports both unencrypted (ws://) and encrypted (wss://) sockets, complete with connection event handling, payload framing, and callback registers. Since networking functions block execution, WebSocket servers are typically run concurrently (e.g. using V's coroutines/threads via the go keyword) to handle incoming messages in the background while keeping the main loop responsive.
This example demonstrates spinning up a local WebSocket server, connecting a WebSocket client to it, exchanging messages, and closing the connection cleanly.
This example demonstrates spinning up a local WebSocket server, connecting a WebSocket client to it, exchanging messages, and closing the connection cleanly using the net.websocket module.
module main
import net.websocket
import time
fn main() {
println('=== net.websocket Module Demo ===')
port := 30099
uri := 'ws://localhost:${port}'
// 1. Initialize and run a local WebSocket server in a separate thread
mut ws_server := websocket.new_server(.ip, port, '/')
ws_server.on_connect(fn (mut s websocket.ServerClient) !bool {
println('Server: Client connecting from ${s.client_key}')
return true
})!
ws_server.on_message(fn (mut ws websocket.Client, msg &websocket.Message) ! {
if msg.opcode == .text_frame {
payload := msg.payload.bytestr()
println('Server received text: "${payload}"')
// Echo message back to client
ws.write_string('Echo: ' + payload)!
}
})
// Run the server listening loop in a background thread
spawn fn [mut ws_server] () {
ws_server.listen() or { println('Server error: ${err}') }
}()
// Allow the server a moment to start
time.sleep(100 * time.millisecond)
// 2. Initialize the WebSocket client
mut ws_client := websocket.new_client(uri) or {
println('Client init failed: ${err}')
return
}
ws_client.on_open(fn (mut c websocket.Client) ! {
println('Client: Connection opened!')
})
ws_client.on_message(fn (mut c websocket.Client, msg &websocket.Message) ! {
if msg.opcode == .text_frame {
payload := msg.payload.bytestr()
println('Client received text response: "${payload}"')
}
})
ws_client.on_error(fn (mut c websocket.Client, error_msg string) ! {
println('Client error: ${error_msg}')
})
// 3. Connect and run the client
ws_client.connect() or {
println('Client failed to connect: ${err}')
return
}
// Start the client listen loop in a background thread
spawn ws_client.listen()
// 4. Send a test message
time.sleep(50 * time.millisecond)
msg_to_send := 'Hello WebSocket Server!'
println('Client sending: "${msg_to_send}"')
ws_client.write_string(msg_to_send) or { println('Client failed to send: ${err}') }
// Wait for echo to arrive
time.sleep(200 * time.millisecond)
// Clean close
println('Client closing connection...')
ws_client.close(1000, 'Done') or { println('Client close error: ${err}') }
time.sleep(50 * time.millisecond)
println('WebSocket Demo finished.')
}Websocket Persistent
In plain words: Builds a persistent, two-way real-time WebSocket server and client that exchange multiple messages over a single stays-open connection.
This example demonstrates a persistent WebSocket connection with structured JSON message routing, validation of payload size, and connection closure on limit violation.
What happens in this example:
- Code note: WsMessage represents a structured application-level WebSocket message.
- Code note: ClientState maintains state for the WebSocket client across callbacks.
- Code note: 1. Initialize and run local WebSocket server
- Code note: Server message handler: validates payload size, decodes JSON, and routes actions.
module main
import net.websocket
import time
import json2
// WsMessage represents a structured application-level WebSocket message.
struct WsMessage {
pub:
action string
data string
}
// ClientState maintains state for the WebSocket client across callbacks.
struct ClientState {
mut:
count int
}
fn main() {
println('=== Persistent WebSocket Protocol Demo ===')
port := 38292
uri := 'ws://localhost:${port}'
// 1. Initialize and run local WebSocket server
mut ws_server := websocket.new_server(.ip, port, '/')
ws_server.on_connect(fn (mut s websocket.ServerClient) !bool {
println('Server: Client connected from ${s.client_key}')
return true
})!
// Server message handler: validates payload size, decodes JSON, and routes actions.
ws_server.on_message(fn (mut ws websocket.Client, msg &websocket.Message) ! {
if msg.opcode == .text_frame {
payload := msg.payload.bytestr()
// Safety check: Enforce maximum payload size limit (e.g., 2048 bytes) to prevent DoS (OOM)
max_allowed_len := 2048
if payload.len > max_allowed_len {
println('Server: Rejected message of size ${payload.len} (exceeds ${max_allowed_len} limit)')
// Close connection with code 1009 (Message Too Big)
ws.close(1009, 'Message size exceeds limit') or {}
return
}
// Decode the JSON protocol message
ws_msg := json2.decode[WsMessage](payload) or {
println('Server: Invalid JSON protocol: ${err}')
err_resp := json2.encode(WsMessage{ action: 'error', data: 'invalid json' })
ws.write_string(err_resp) or {}
return
}
println('Server received action "${ws_msg.action}" with data (len: ${ws_msg.data.len})')
match ws_msg.action {
'ping' {
resp := json2.encode(WsMessage{ action: 'pong', data: ws_msg.data })
ws.write_string(resp)!
}
'goodbye' {
println('Server received goodbye action. Replying and closing...')
resp := json2.encode(WsMessage{ action: 'goodbye_ack', data: 'Goodbye!' })
ws.write_string(resp)!
// Clean close from server side
ws.close(1000, 'Normal Closure') or {}
}
else {
println('Server: Unknown action: ${ws_msg.action}')
}
}
}
})
ws_server.on_close(fn (mut ws websocket.Client, code int, reason string) ! {
println('Server: Client disconnected (code: ${code}, reason: "${reason}")')
})
// Start server listening in background thread
spawn ws_server.listen()
// Allow server time to bind and listen
time.sleep(200 * time.millisecond)
// 2. Client 1: Demonstrates persistent conversational ping-pong loop
println('\n--- Starting Client 1 (Conversational Loop) ---')
mut ws_client1 := websocket.new_client(uri)!
mut state1 := &ClientState{
count: 0
}
ws_client1.on_open(fn (mut c websocket.Client) ! {
println('Client 1: Connection opened!')
// Initiate the first Ping message
ping_msg := json2.encode(WsMessage{ action: 'ping', data: '1' })
c.write_string(ping_msg)!
})
ws_client1.on_message(fn [mut state1] (mut c websocket.Client, msg &websocket.Message) ! {
if msg.opcode == .text_frame {
payload := msg.payload.bytestr()
ws_msg := json2.decode[WsMessage](payload) or { return }
println('Client 1 received response action "${ws_msg.action}" with data: "${ws_msg.data}"')
if ws_msg.action == 'pong' {
state1.count++
if state1.count < 3 {
next_ping := json2.encode(WsMessage{ action: 'ping', data: '${state1.count + 1}' })
println('Client 1 sending: "${next_ping}"')
c.write_string(next_ping)!
} else {
goodbye := json2.encode(WsMessage{ action: 'goodbye', data: 'Goodbye' })
println('Client 1 sending goodbye: "${goodbye}"')
c.write_string(goodbye)!
}
} else if ws_msg.action == 'goodbye_ack' {
println('Client 1 received goodbye ack. Client closing connection.')
c.close(1000, 'Done') or {}
}
}
})
ws_client1.on_close(fn (mut c websocket.Client, code int, reason string) ! {
println('Client 1: Connection closed (code: ${code}, reason: "${reason}")')
})
ws_client1.on_error(fn (mut c websocket.Client, error_msg string) ! {
println('Client 1 error: ${error_msg}')
})
ws_client1.connect() or {
println('Client 1 failed to connect: ${err}')
return
}
spawn ws_client1.listen()
// Wait for the first flow to complete
time.sleep(600 * time.millisecond)
// 3. RUN CLIENT CONNECTION 2: Reject oversized message
println('\n--- Connection 2: Security Validation (Oversized Message) ---')
mut ws_client2 := websocket.new_client(uri) or {
println('Client 2 init failed: ${err}')
return
}
ws_client2.on_open(fn (mut c websocket.Client) ! {
println('Client 2: Connection opened!')
// Send oversized data (3000 bytes, exceeding server 2048-byte limit)
large_payload := 'A'.repeat(3000)
large_msg := json2.encode(WsMessage{ action: 'ping', data: large_payload })
println('Client 2 sending oversized payload (size: ${large_msg.len} bytes)...')
c.write_string(large_msg)!
})
ws_client2.on_close(fn (mut c websocket.Client, code int, reason string) ! {
println('Client 2: Connection closed (code: ${code}, reason: "${reason}")')
if code == 1009 {
println('Client 2: Successfully verified server rejected oversized message with code 1009!')
} else if code == 1000 {
// Ignore standard teardown close
} else {
println('Client 2: Unexpected close code: ${code}')
}
})
ws_client2.on_error(fn (mut c websocket.Client, error_msg string) ! {
println('Client 2 error: ${error_msg}')
})
ws_client2.connect() or {
println('Client 2 failed to connect: ${err}')
return
}
spawn ws_client2.listen()
// Wait for the second flow to finish
time.sleep(500 * time.millisecond)
// Clean close of server listener
println('\nWebSocket Protocol Demo finished.')
}Net Html
In plain words: The net.html module parses raw HTML web pages into a searchable tree of tags (get_tag, get_tag_by_attribute) so you can extract links or text easily.
The standard library net.html module provides light-weight parsing and querying APIs for HTML documents. It parses an HTML raw string into a structured hierarchical Document Object Model (DOM) tree of nodes. Developers can query this parsed DOM tree using methods to find tags by element name, filter by specific CSS class names, or read attributes (e.g. href inside <a> tags). This is extremely useful for building web scrapers, crawler services, or content extractors without needing external dependencies.
This example illustrates parsing an HTML string, navigating node structures, filtering tags by attributes, and printing text values.
This example demonstrates parsing HTML strings, querying tags by class name and attribute values, and extracting node text and properties using the net.html module.
module main
import net.html
fn main() {
println('=== net.html Module Demo ===')
// 1. Define a sample HTML document to parse
html_content := '
<!DOCTYPE html>
<html>
<head>
<title>V Programming Language</title>
</head>
<body>
<header>
<h1 class="main-title">Welcome to the V Standard Library</h1>
</header>
<main>
<div class="content" id="overview">
<p class="description">
V is a simple, fast, safe, and compiled language.
</p>
<p class="description">
The net.html module parses HTML into a Queryable Document Object Model (DOM).
</p>
<a href="https://vlang.io" class="link-btn" id="home-link">Official Website</a>
<a href="https://github.com/vlang/v" class="link-btn" id="repo-link">GitHub Repository</a>
</div>
</main>
</body>
</html>'
// 2. Parse the HTML string into a DocumentObjectModel (DOM)
println('Parsing HTML document...')
dom := html.parse(html_content)
// 3. Retrieve tags by name (e.g., header, title)
title_tags := dom.get_tags(name: 'title')
if title_tags.len > 0 {
println('Page Title: "${title_tags[0].text()}"')
}
// 4. Retrieve tags by class name
descriptions := dom.get_tags_by_class_name('description')
println('\nParagraphs with class "description":')
for i, p in descriptions {
println(' ${i + 1}: ${p.text().trim_space()}')
}
// 5. Query tags by attribute value (e.g., href, id)
links := dom.get_tags_by_class_name('link-btn')
println('\nLinks found in document:')
for link in links {
href := link.attributes['href']
id := link.attributes['id']
text := link.text()
println(' - Text: "${text}"')
println(' ID: "${id}"')
println(' URL: "${href}"')
}
// 6. Access DOM root and verify serialization representation
root := dom.get_root()
println('\nDOM Root Element Tag Name: <${root.name}>')
println('HTML Demo finished.')
}Net Jsonrpc
In plain words: Shows how JSON-RPC wraps function names and parameters into a standard JSON message format for client-server communication.
This example demonstrates implementing JSON-RPC 2.0 servers and clients using V's net.jsonrpc module, utilizing a Unix domain socket connection as the transport layer.
What happens in this example:
- Code note: Define structs for request parameters and response results
- Code note: Router handler to compute mathematical operations
- Code note: Decode request parameters into MathParams struct
- Code note: Write the successful result back
module main
import net.unix
import net.jsonrpc
import os
import time
// Define structs for request parameters and response results
struct MathParams {
pub:
a int
b int
}
struct MathResult {
pub:
sum int
difference int
}
// Router handler to compute mathematical operations
fn handle_math(req &jsonrpc.Request, mut wr jsonrpc.ResponseWriter) {
// Decode request parameters into MathParams struct
params := req.decode_params[MathParams]() or {
wr.write_error(jsonrpc.invalid_params)
return
}
result := MathResult{
sum: params.a + params.b
difference: params.a - params.b
}
// Write the successful result back
wr.write(result)
}
// Start JSON-RPC 2.0 Server over Unix Socket
fn run_rpc_server(socket_path string) ! {
// Ensure cleanup of any old socket file
if os.exists(socket_path) {
os.rm(socket_path)!
}
mut listener := unix.listen_stream(socket_path, unix.ListenOptions{}) or {
println('Server: Failed to listen: ${err}')
return err
}
defer {
listener.close() or {}
listener.unlink() or {}
}
println('Server: Listening and waiting for connections...')
// Accept client connection
mut conn := listener.accept() or {
println('Server: Accept failed: ${err}')
return err
}
defer {
conn.close() or {}
}
println('Server: Client connected, initiating JSON-RPC protocol.')
// Setup JSON-RPC router and register math method
mut router := jsonrpc.Router{}
router.register('math.compute', handle_math)
// Create JSON-RPC server wrapping the Unix socket connection stream
mut server := jsonrpc.new_server(jsonrpc.ServerConfig{
stream: conn
handler: router.handle_jsonrpc
})
// Process incoming request and respond
server.respond() or {
println('Server: Error processing request: ${err}')
return err
}
println('Server: Successfully processed request and shut down.')
}
// Start JSON-RPC 2.0 Client over Unix Socket
fn run_rpc_client(socket_path string) ! {
println('Client: Connecting to server at ${socket_path}...')
mut conn := unix.connect_stream(socket_path) or {
println('Client: Connection failed: ${err}')
return err
}
defer {
conn.close() or {}
}
// Create JSON-RPC client wrapping the Unix socket connection stream
mut client := jsonrpc.new_client(jsonrpc.ClientConfig{
stream: conn
})
params := MathParams{
a: 45
b: 17
}
println('Client: Sending request "math.compute" with params {a: ${params.a}, b: ${params.b}}')
// Execute JSON-RPC request (method, parameters, request ID)
resp := client.request('math.compute', params, 'req_math_1') or {
println('Client: Request execution failed: ${err}')
return err
}
// Decode response result
result := resp.decode_result[MathResult]() or {
println('Client: Failed to decode response result: ${err}')
return err
}
println('Client received response:')
println(' Request ID: ${resp.id}')
println(' Result sum: ${result.sum}')
println(' Result difference: ${result.difference}')
}
fn main() {
println('=== net.jsonrpc Module Demo ===')
socket_path := os.join_path(os.temp_dir(), 'v_jsonrpc_example_socket')
// Spawn JSON-RPC server in background thread
spawn fn (path string) {
run_rpc_server(path) or { println('Server thread failed: ${err}') }
}(socket_path)
// Wait briefly for the server socket to bind
time.sleep(100 * time.millisecond)
// Run JSON-RPC client in main thread
run_rpc_client(socket_path) or { println('Client thread failed: ${err}') }
// Wait briefly for server post-handling cleanups
time.sleep(50 * time.millisecond)
println('JSON-RPC Demo finished.')
}Net Jsonrpc Persistent
In plain words: Demonstrates a persistent JSON-RPC 2.0 server and client where remote function calls (add, subtract) are sent as JSON messages over TCP.
This example demonstrates how to build a persistent JSON-RPC 2.0 connection. The server calls server.start() to continuously process incoming requests, and the client sends multiple method invocations sequentially over a single socket connection.
module main
import net.unix
import net.jsonrpc
import os
import time
// Define structs for request parameters and response results
struct MathParams {
pub:
a int
b int
}
struct MathResult {
pub:
sum int
difference int
}
// Router handler to compute mathematical operations
fn handle_math(req &jsonrpc.Request, mut wr jsonrpc.ResponseWriter) {
params := req.decode_params[MathParams]() or {
wr.write_error(jsonrpc.invalid_params)
return
}
result := MathResult{
sum: params.a + params.b
difference: params.a - params.b
}
wr.write(result)
}
// Start JSON-RPC 2.0 Server over Unix Socket in a persistent loop
fn run_rpc_server(socket_path string) ! {
if os.exists(socket_path) {
os.rm(socket_path)!
}
mut listener := unix.listen_stream(socket_path, unix.ListenOptions{}) or {
println('Server: Failed to listen: ${err}')
return err
}
defer {
listener.close() or {}
listener.unlink() or {}
}
println('Server: Listening and waiting for connections...')
mut conn := listener.accept() or {
println('Server: Accept failed: ${err}')
return err
}
defer {
conn.close() or {}
}
println('Server: Client connected, starting persistent JSON-RPC loop.')
mut router := jsonrpc.Router{}
router.register('math.compute', handle_math)
mut server := jsonrpc.new_server(jsonrpc.ServerConfig{
stream: conn
handler: router.handle_jsonrpc
})
// Start the server processing loop (calls s.respond() in a loop)
server.start()
println('Server: Loop finished.')
}
// Start JSON-RPC 2.0 Client and make multiple requests over the same connection
fn run_rpc_client(socket_path string) ! {
println('Client: Connecting to server at ${socket_path}...')
mut conn := unix.connect_stream(socket_path) or {
println('Client: Connection failed: ${err}')
return err
}
defer {
conn.close() or {}
}
mut client := jsonrpc.new_client(jsonrpc.ClientConfig{
stream: conn
})
// Perform multiple requests over the same persistent connection
for i in 1 .. 4 {
params := MathParams{
a: 10 * i
b: 5 * i
}
req_id := 'req_math_${i}'
println('Client: Sending request "${req_id}" for math.compute with {a: ${params.a}, b: ${params.b}}')
resp := client.request('math.compute', params, req_id) or {
println('Client: Request failed: ${err}')
return err
}
result := resp.decode_result[MathResult]() or {
println('Client: Failed to decode result: ${err}')
return err
}
println('Client received response for ${resp.id}:')
println(' sum: ${result.sum}')
println(' difference: ${result.difference}')
time.sleep(50 * time.millisecond)
}
}
fn main() {
println('=== Persistent net.jsonrpc Module Demo ===')
socket_path := os.join_path(os.temp_dir(), 'v_jsonrpc_persistent_socket')
// Spawn JSON-RPC server in background thread
spawn fn (path string) {
run_rpc_server(path) or { println('Server thread failed: ${err}') }
}(socket_path)
// Wait briefly for the server socket to bind
time.sleep(100 * time.millisecond)
// Run JSON-RPC client in main thread
run_rpc_client(socket_path) or { println('Client thread failed: ${err}') }
// Wait briefly for server post-handling cleanups
time.sleep(50 * time.millisecond)
println('Persistent JSON-RPC Demo finished.')
}Net Ssl
In plain words: Shows how to wrap a network connection in SSL/TLS encryption (net.ssl / net.mbedtls) so data traveling over the network cannot be snooped.
This example demonstrates setting up a secure SSL/TLS server and client connection using the standard library's net.mbedtls module, including programmatically generating a self-signed key/cert pair using OpenSSL and cleaning them up on exit.
module main
import net.mbedtls
import net
import os
import time
// generate_certs runs openssl to create a temporary self-signed certificate and key.
fn generate_certs() ! {
println('Generating temporary self-signed SSL certificate...')
res := os.execute('openssl req -x509 -newkey rsa:2048 -keyout temp_server.key -out temp_server.crt -days 1 -nodes -subj "/CN=localhost"')
if res.exit_code != 0 {
return error('Failed to generate certs: ${res.output}')
}
}
// cleanup_certs deletes the temporary certificate and key files.
fn cleanup_certs() {
println('Cleaning up temporary certificate files...')
os.rm('temp_server.key') or {}
os.rm('temp_server.crt') or {}
}
// run_server starts the SSL server, accepts a client connection,
// reads a message, responds securely, and exits.
fn run_server(port int) ! {
config := mbedtls.SSLConnectConfig{
cert: 'temp_server.crt'
cert_key: 'temp_server.key'
validate: false
}
mut listener := mbedtls.new_ssl_listener('127.0.0.1:${port}', config) or {
println('Server: Failed to create listener: ${err}')
return err
}
defer {
listener.shutdown() or {}
}
println('Server: Listening on SSL port ${port}...')
mut conn := listener.accept() or {
println('Server: Failed to accept SSL connection: ${err}')
return err
}
defer {
conn.close() or {}
}
println('Server: SSL Client connected!')
mut buf := []u8{len: 1024}
n := conn.read(mut buf) or {
println('Server: Read failed: ${err}')
return err
}
message := buf[..n].bytestr()
println('Server: Received message: "${message}"')
// Respond securely
response := 'Echo Secure: ${message}'
conn.write(response.bytes()) or {
println('Server: Write failed: ${err}')
return err
}
println('Server: Sent secure response.')
}
// run_client connects to the server port via TCP first, wraps it in SSL,
// sends a message, reads the secure response, and closes.
fn run_client(port int) ! {
println('Client: Dialing standard TCP port first...')
mut tcp_conn := net.dial_tcp('127.0.0.1:${port}') or {
println('Client: Failed to connect standard TCP: ${err}')
return err
}
println('Client: Initiating SSL handshake on top of TCP connection...')
config := mbedtls.SSLConnectConfig{
validate: false
}
mut ssl_conn := mbedtls.new_ssl_conn(config) or {
println('Client: Failed to create SSL connection struct: ${err}')
return err
}
defer {
ssl_conn.close() or {}
}
ssl_conn.connect(mut tcp_conn, 'localhost') or {
println('Client: SSL handshake failed: ${err}')
return err
}
println('Client: Secure connection established!')
message := 'Hello V Secure Sockets!'
println('Client: Sending message: "${message}"')
ssl_conn.write(message.bytes()) or {
println('Client: Write failed: ${err}')
return err
}
mut buf := []u8{len: 1024}
n := ssl_conn.read(mut buf) or {
println('Client: Read failed: ${err}')
return err
}
response := buf[..n].bytestr()
println('Client: Received response: "${response}"')
}
fn main() {
println('=== net.ssl Module Demo ===')
generate_certs() or {
println('Error generating certs: ${err}')
return
}
defer {
cleanup_certs()
}
port := 38295
// Spawn server in background
spawn fn (p int) {
run_server(p) or { println('Server thread failed: ${err}') }
}(port)
// Wait briefly for server to bind
time.sleep(200 * time.millisecond)
// Run client in main thread
run_client(port) or { println('Client failed: ${err}') }
time.sleep(50 * time.millisecond)
println('SSL Demo finished.')
}Ssl Persistent
In plain words: Demonstrates an encrypted SSL/TLS connection where the client and server exchange multiple secure messages and keep session state.
This example demonstrates keeping an SSL/TLS connection open for multiple rounds of secure back-and-forth ping-pong communication over net.mbedtls.
What happens in this example:
- Code note: generate_certs runs openssl to create a temporary self-signed certificate and key.
- Code note: cleanup_certs deletes the temporary certificate and key files.
- Code note: run_server starts the SSL server, accepts a client connection,
- Code note: and processes incoming messages in a loop until the client sends "Goodbye".
module main
import net.mbedtls
import net
import os
import time
// generate_certs runs openssl to create a temporary self-signed certificate and key.
fn generate_certs() ! {
println('Generating temporary self-signed SSL certificate...')
res := os.execute('openssl req -x509 -newkey rsa:2048 -keyout temp_server.key -out temp_server.crt -days 1 -nodes -subj "/CN=localhost"')
if res.exit_code != 0 {
return error('Failed to generate certs: ${res.output}')
}
}
// cleanup_certs deletes the temporary certificate and key files.
fn cleanup_certs() {
println('Cleaning up temporary certificate files...')
os.rm('temp_server.key') or {}
os.rm('temp_server.crt') or {}
}
// run_server starts the SSL server, accepts a client connection,
// and processes incoming messages in a loop until the client sends "Goodbye".
fn run_server(port int) ! {
config := mbedtls.SSLConnectConfig{
cert: 'temp_server.crt'
cert_key: 'temp_server.key'
validate: false
}
mut listener := mbedtls.new_ssl_listener('127.0.0.1:${port}', config) or {
println('Server: Failed to create listener: ${err}')
return err
}
defer {
listener.shutdown() or {}
}
println('Server: Listening on SSL port ${port}...')
mut conn := listener.accept() or {
println('Server: Failed to accept SSL connection: ${err}')
return err
}
defer {
conn.close() or {}
}
println('Server: SSL Client connected!')
// Loop to handle back-and-forth messages on the same secure connection
for {
mut buf := []u8{len: 1024}
n := conn.read(mut buf) or {
println('Server: Secure connection closed or read error: ${err}')
break
}
if n == 0 {
println('Server: Client disconnected.')
break
}
message := buf[..n].bytestr()
println('Server received secure: "${message}"')
if message == 'Goodbye' {
println('Server received Goodbye. Replying and closing secure connection...')
conn.write('Goodbye!'.bytes()) or { println('Server: Write failed: ${err}') }
break
}
response := 'Echo: ${message}'
println('Server sending secure: "${response}"')
conn.write(response.bytes()) or {
println('Server: Write failed: ${err}')
break
}
}
println('Server finished.')
}
// run_client connects to the server port via TCP first, wraps it in SSL,
// and sends multiple messages in a loop before ending the secure session cleanly.
fn run_client(port int) ! {
println('Client: Dialing standard TCP port first...')
mut tcp_conn := net.dial_tcp('127.0.0.1:${port}') or {
println('Client: Failed to connect standard TCP: ${err}')
return err
}
println('Client: Initiating SSL handshake on top of TCP connection...')
config := mbedtls.SSLConnectConfig{
validate: false
}
mut ssl_conn := mbedtls.new_ssl_conn(config) or {
println('Client: Failed to create SSL connection struct: ${err}')
return err
}
defer {
ssl_conn.close() or {}
}
ssl_conn.connect(mut tcp_conn, 'localhost') or {
println('Client: SSL handshake failed: ${err}')
return err
}
println('Client: Secure connection established!')
// Exchange multiple messages
for i in 1 .. 4 {
message := 'Ping ${i}'
println('Client sending secure: "${message}"')
ssl_conn.write(message.bytes()) or {
println('Client: Write failed: ${err}')
return err
}
// Read response
mut buf := []u8{len: 1024}
n := ssl_conn.read(mut buf) or {
println('Client: Read failed: ${err}')
return err
}
if n == 0 {
println('Client: Server closed secure connection.')
return error('Server closed connection unexpectedly')
}
response := buf[..n].bytestr()
println('Client received secure response: "${response}"')
time.sleep(50 * time.millisecond)
}
// Send Goodbye to cleanly terminate the persistent session
println('Client sending: "Goodbye"')
ssl_conn.write('Goodbye'.bytes()) or {
println('Client: Write failed: ${err}')
return err
}
mut buf := []u8{len: 1024}
n := ssl_conn.read(mut buf) or {
println('Client: Read failed: ${err}')
return err
}
if n > 0 {
response := buf[..n].bytestr()
println('Client received secure response: "${response}"')
}
println('Client finished.')
}
fn main() {
println('=== Persistent SSL Demo ===')
generate_certs() or {
println('Error generating certs: ${err}')
return
}
defer {
cleanup_certs()
}
port := 38296
// Spawn the server in a background thread
spawn fn (p int) {
run_server(p) or { println('Server thread failed: ${err}') }
}(port)
// Allow the server thread a short time to start and bind
time.sleep(200 * time.millisecond)
// Run the client in the main thread
run_client(port) or { println('Client failed: ${err}') }
// Give the server a small window to finish deferred cleanups
time.sleep(50 * time.millisecond)
println('SSL Sockets Demo finished.')
}Net Tcp
In plain words: Demonstrates a classic TCP client and server (net.listen_tcp and net.dial_tcp)โthe reliable building block behind HTTP, SSH, and databases.
The net standard library module provides full support for TCP (Transmission Control Protocol) stream networking. TCP is a connection-oriented, reliable transport protocol that guarantees ordered, error-checked delivery of streams of octets between hosts. In V, you construct a TCP server using net.listen_tcp which binds to an IP address and port, returning a listener instance. Calling accept() on the listener blocks execution until an incoming client initiates a connection. Communication is performed via stream reading and writing operations directly on the connection sockets.
This example illustrates creating a TCP server and client, opening socket connections, sending payload data, and handling connection cleanup blocks.
This example demonstrates how to create a simple TCP server and client in V. The server listens on a local port, accepts an incoming client connection, receives data, sends a response, and closes the connection.
module main
import net
import time
// run_server starts the TCP server on the specified port, accepts a connection,
// reads a message, responds, and closes the connection.
fn run_server(port int) ! {
mut listener := net.listen_tcp(.ip, '127.0.0.1:${port}') or {
println('Server: Failed to listen on port ${port}: ${err}')
return err
}
defer {
listener.close() or {}
}
println('Server: Listening on 127.0.0.1:${port}...')
mut conn := listener.accept() or {
println('Server: Failed to accept connection: ${err}')
return err
}
defer {
conn.close() or {}
}
println('Server: Client connected!')
mut buf := []u8{len: 1024}
n := conn.read(mut buf) or {
println('Server: Read failed: ${err}')
return err
}
message := buf[..n].bytestr()
println('Server: Received message: "${message}"')
// Write response back to the client
response := 'Echo: ${message}'
conn.write(response.bytes()) or {
println('Server: Write failed: ${err}')
return err
}
println('Server: Sent echo response.')
}
// run_client connects to the TCP server, sends a message, reads the response,
// and closes the connection.
fn run_client(port int) ! {
println('Client: Connecting to 127.0.0.1:${port}...')
mut conn := net.dial_tcp('127.0.0.1:${port}') or {
println('Client: Failed to connect: ${err}')
return err
}
defer {
conn.close() or {}
}
println('Client: Connected!')
// Send message to the server
message := 'Hello V TCP Sockets!'
println('Client: Sending message: "${message}"')
conn.write(message.bytes()) or {
println('Client: Write failed: ${err}')
return err
}
// Read server response
mut buf := []u8{len: 1024}
n := conn.read(mut buf) or {
println('Client: Read failed: ${err}')
return err
}
response := buf[..n].bytestr()
println('Client: Received response: "${response}"')
}
fn main() {
println('=== net.tcp Module Demo ===')
port := 38290
// Spawn the server in a background thread
spawn fn (p int) {
run_server(p) or { println('Server thread failed: ${err}') }
}(port)
// Allow the server thread a short time to start and bind
time.sleep(100 * time.millisecond)
// Run the client in the main thread
run_client(port) or { println('Client failed: ${err}') }
// Give the server a small window to finish deferred cleanups
time.sleep(50 * time.millisecond)
println('TCP Demo finished.')
}Tcp Persistent
In plain words: Builds a multi-command TCP server (PING, ECHO, TIME, QUIT) that keeps the socket open across multiple client requests.
This example demonstrates a persistent length-prefixed TCP connection, processing payloads in chunks, and rejecting messages exceeding safety size boundaries.
What happens in this example:
- Code note: write_msg sends a message using a 4-byte magic signature and a 4-byte big-endian length in a single write syscall.
- Code note: Send consolidated buffer in a single system call
- Code note: read_exact reads exactly
sizebytes from the connection, processing data in chunks. - Code note: Real-world performance optimization: Reads directly into mutable slice views of our pre-allocated
module main
import net
import time
const max_message_size = 8192
// write_msg sends a message using a 4-byte magic signature and a 4-byte big-endian length in a single write syscall.
fn write_msg(mut conn net.TcpConn, payload string) ! {
mut buf := []u8{len: 8 + payload.len}
buf[0] = `M`
buf[1] = `S`
buf[2] = `G`
buf[3] = `0`
buf[4] = u8((u32(payload.len) >> 24) & 0xff)
buf[5] = u8((u32(payload.len) >> 16) & 0xff)
buf[6] = u8((u32(payload.len) >> 8) & 0xff)
buf[7] = u8(u32(payload.len) & 0xff)
if payload.len > 0 {
unsafe {
C.memcpy(&buf[8], payload.str, payload.len)
}
}
// Send consolidated buffer in a single system call
conn.write(buf) or { return err }
}
// read_exact reads exactly `size` bytes from the connection, processing data in chunks.
// Real-world performance optimization: Reads directly into mutable slice views of our pre-allocated
// buffer to achieve zero-allocation reads inside the chunking loop.
fn read_exact(mut conn net.TcpConn, size int) ![]u8 {
mut data := []u8{len: size}
mut read_bytes := 0
for read_bytes < size {
remaining := size - read_bytes
// Use a small buffer chunk limit (e.g. 512 bytes) to demonstrate reading in chunks
chunk_limit := if remaining > 512 { 512 } else { remaining }
n := conn.read(mut data[read_bytes..read_bytes + chunk_limit]) or { return err }
if n == 0 {
if read_bytes == 0 {
return error('EOF')
}
return error('unexpected end of stream')
}
read_bytes += n
}
return data
}
// read_msg reads a single framed message.
fn read_msg(mut conn net.TcpConn, max_size int) !string {
// Read header: 4 magic bytes + 4 length bytes = 8 bytes
header_bytes := read_exact(mut conn, 8) or { return err }
// Validate protocol magic bytes
if header_bytes[0] != `M` || header_bytes[1] != `S` || header_bytes[2] != `G`
|| header_bytes[3] != `0` {
return error('invalid protocol magic bytes')
}
// Reconstruct big-endian length
len := int((u32(header_bytes[4]) << 24) | (u32(header_bytes[5]) << 16) | (u32(header_bytes[6]) << 8) | u32(header_bytes[7]))
// Real-world security boundary: Reject messages larger than allowed limit to prevent DoS (OOM)
if len > max_size {
return error('message size ${len} exceeds limit of ${max_size} bytes')
}
if len < 0 {
return error('invalid negative message length')
}
// Read the actual payload
payload_bytes := read_exact(mut conn, len) or { return err }
return payload_bytes.bytestr()
}
// run_server starts the TCP server on the specified port, accepts a connection,
// and processes incoming messages in a loop according to our framing protocol.
fn run_server(port int) ! {
mut listener := net.listen_tcp(.ip, '127.0.0.1:${port}') or {
println('Server: Failed to listen on port ${port}: ${err}')
return err
}
defer {
listener.close() or {}
}
println('Server: Listening on 127.0.0.1:${port}...')
mut conn := listener.accept() or {
println('Server: Failed to accept connection: ${err}')
return err
}
defer {
conn.close() or {}
}
println('Server: Client connected!')
// Real-world safety practice: Set read and write timeouts to prevent connection hang-ups (Slowloris DoS)
conn.set_read_timeout(time.second * 5)
conn.set_write_timeout(time.second * 5)
for {
message := read_msg(mut conn, max_message_size) or {
if err.msg() == 'EOF' {
println('Server: Client disconnected cleanly (EOF).')
} else {
println('Server: Connection closed or protocol error: ${err}')
}
break
}
// Preview message content
preview_len := if message.len > 30 { 30 } else { message.len }
println('Server received message (len: ${message.len}): "${message[..preview_len]}"...')
if message == 'Goodbye' {
println('Server received Goodbye. Replying and closing connection...')
write_msg(mut conn, 'Goodbye!') or { println('Server: Write failed: ${err}') }
break
}
response := 'Echo: ${message}'
write_msg(mut conn, response) or {
println('Server: Write failed: ${err}')
break
}
}
println('Server finished.')
}
// run_client connects to the TCP server, sends multiple messages (including
// a large chunked message and an invalid/overflow message), and validates responses.
fn run_client(port int) ! {
println('Client: Connecting to 127.0.0.1:${port}...')
mut conn := net.dial_tcp('127.0.0.1:${port}') or {
println('Client: Failed to connect: ${err}')
return err
}
defer {
conn.close() or {}
}
println('Client: Connected!')
// Set connection timeouts for the client too
conn.set_read_timeout(time.second * 5)
conn.set_write_timeout(time.second * 5)
// 1. Send a standard small message
msg1 := 'Ping 1'
println('Client sending small message: "${msg1}"')
write_msg(mut conn, msg1)!
resp1 := read_msg(mut conn, max_message_size)!
println('Client received response: "${resp1}"')
time.sleep(50 * time.millisecond)
// 2. Send a large message within limit (5000 bytes) to trigger chunked read assembly
msg2 := 'A'.repeat(5000)
println('Client sending large message of length ${msg2.len}...')
write_msg(mut conn, msg2)!
resp2 := read_msg(mut conn, max_message_size)!
println('Client received response of length ${resp2.len} successfully!')
time.sleep(50 * time.millisecond)
// 3. Attempt to send an invalid/overflow message (header length > max_message_size)
println('Client sending invalid header claiming 100,000 bytes payload...')
magic := [u8(`M`), `S`, `G`, `0`]
bad_len_bytes := [u8(0), 1, 134, 160] // 100,000 big-endian
conn.write(magic)!
conn.write(bad_len_bytes)!
// The server must reject the message and terminate the connection
mut buf := []u8{len: 1}
n := conn.read(mut buf) or {
println('Client: Successfully verified server rejected overflow and closed connection: ${err}')
return
}
if n == 0 {
println('Client: Successfully verified server rejected overflow (EOF received).')
} else {
println('Client: Warning - Server did not close connection on overflow!')
}
}
fn main() {
println('=== Persistent TCP Protocol Demo ===')
port := 38293
// Spawn the server in a background thread
spawn fn (p int) {
run_server(p) or { println('Server thread failed: ${err}') }
}(port)
// Allow the server thread a short time to start and bind
time.sleep(100 * time.millisecond)
// Run the client in the main thread
run_client(port) or { println('Client failed: ${err}') }
// Give the server a small window to finish deferred cleanups
time.sleep(50 * time.millisecond)
println('TCP Protocol Demo finished.')
}Deep Dive Explanation: Persistent TCP & Custom Protocol Framing
1. Why Message Framing is Essential
TCP is a byte-stream protocol. It does not have any concept of packet or message boundaries; it guarantees only that bytes arrive in order. If a client writes two messages of 100 bytes, the server might read them as a single chunk of 200 bytes, or as several chunks of arbitrary sizes (e.g., 50 and 150 bytes).
To transmit individual messages safely over a persistent connection, we define a custom Framing Protocol:
- Magic Bytes (4 bytes): The message starts with a signature (
MSG0in ASCII). This acts as a sanity check. If the server receives something else, it knows the stream is corrupted or the client is using an incorrect protocol. - Length Prefix (4 bytes): A big-endian 32-bit integer indicating the exact size of the following payload.
- Payload: The actual raw message bytes.
2. Zero-Allocation Chunked Reading (`read_exact`)
The read_exact(mut conn, size) function reads bytes in a loop until the full requested size is reached:
n := conn.read(mut data[read_bytes .. read_bytes + chunk_limit])- Performance Optimization: Instead of allocating new buffers in each iteration of the loop, V uses slice expressions (
data[start..end]) to pass a mutable reference to a specific sub-range of the pre-allocateddataarray directly to the socket read function. This avoids any dynamic memory allocation, optimizing throughput and memory usage.
3. Security Boundaries & DoS Protection
Production socket servers must defend against malicious input and network timeouts:
- Max Payload Checking: In
read_msg, the server reads the length prefix from the header. Iflen > max_size(e.g., larger than8192bytes), the server immediately rejects the message and closes the connection. Without this check, a client could claim a message size of 2 GB, forcing the server to allocate a huge array and crash due to Out-Of-Memory (OOM). - Connection Timeouts: Calling
conn.set_read_timeoutandconn.set_write_timeoutprevents threads from blocking indefinitely. If a client connects and then stops sending data (a Slowloris attack), the server will automatically close the socket after the timeout expires (5 seconds in this demo).
Net Udp
In plain words: Demonstrates fast, connectionless UDP datagram messaging (net.listen_udp and net.dial_udp) used in real-time games and video streaming.
UDP (User Datagram Protocol) is a connectionless, unreliable transport protocol that sends independent packets (datagrams) without establishing a dedicated channel. Unlike TCP, UDP has no handshakes, retry logic, or packet ordering guarantees, which makes it highly lightweight and perfect for low-latency tasks like real-time gaming, video streams, or DNS queries. In V, you open a UDP socket using net.listen_udp. Incoming datagrams are read along with their sender IP/port address (using read or recvfrom), and replies are routed back using the connectionless write_to method.
This example illustrates opening a UDP socket listener, sending datagram packets, extracting sender details, and replying to a dynamic port.
This example demonstrates sending and receiving connectionless UDP packets. The server binds to a local port and receives a message along with the sender's address, and responds to it using write_to.
module main
import net
import time
// run_server starts the UDP server on the specified port, listens for a packet,
// prints the message, sends a response back to the sender, and exits.
fn run_server(port int) ! {
mut socket := net.listen_udp('127.0.0.1:${port}') or {
println('Server: Failed to listen on port ${port}: ${err}')
return err
}
defer {
socket.close() or {}
}
println('Server: Listening for UDP packets on port ${port}...')
mut buf := []u8{len: 1024}
read, addr := socket.read(mut buf) or {
println('Server: Read failed: ${err}')
return err
}
message := buf[..read].bytestr()
println('Server: Received message from ${addr}: "${message}"')
// Send echo response
response := 'Echo: ${message}'
socket.write_to(addr, response.bytes()) or {
println('Server: Send failed: ${err}')
return err
}
println('Server: Sent echo response.')
}
// run_client creates a UDP client socket, sends a datagram, and waits for a response.
fn run_client(port int) ! {
mut socket := net.dial_udp('127.0.0.1:${port}') or {
println('Client: Failed to dial server: ${err}')
return err
}
defer {
socket.close() or {}
}
message := 'Hello V UDP Sockets!'
println('Client: Sending message: "${message}"')
socket.write(message.bytes()) or {
println('Client: Write failed: ${err}')
return err
}
// Read server response
mut buf := []u8{len: 1024}
read, addr := socket.read(mut buf) or {
println('Client: Read failed: ${err}')
return err
}
response := buf[..read].bytestr()
println('Client: Received response from ${addr}: "${response}"')
}
fn main() {
println('=== net.udp Module Demo ===')
port := 38291
// Spawn the server in a background thread
spawn fn (p int) {
run_server(p) or { println('Server thread failed: ${err}') }
}(port)
// Allow the server thread a short time to start and bind
time.sleep(100 * time.millisecond)
// Run the client in the main thread
run_client(port) or { println('Client failed: ${err}') }
// Give the server a small window to finish deferred cleanups
time.sleep(50 * time.millisecond)
println('UDP Demo finished.')
}Udp Persistent
In plain words: Shows how to split a larger message into numbered UDP packets (chunks) and reassemble them on the receiving side.
This example demonstrates application-level packet fragmentation, reassembly, and fragment count limit enforcement over UDP.
What happens in this example:
- Code note: UdpReassembler stores state for reassembling fragmented UDP packets.
- Code note: writeudpmsg fragments and sends a message to dialed destination.
- Code note: Real-world performance optimization: Avoids allocating a full payload copy by writing fragments
- Code note: directly from string memory slices using C.memcpy.
module main
import net
import time
// UdpReassembler stores state for reassembling fragmented UDP packets.
struct UdpReassembler {
mut:
fragments map[int][]u8
total int
last_seen time.Time
}
// write_udp_msg fragments and sends a message to dialed destination.
// Real-world performance optimization: Avoids allocating a full payload copy by writing fragments
// directly from string memory slices using C.memcpy.
fn write_udp_msg(mut socket net.UdpConn, payload string) ! {
chunk_size := 1024
total_frags := (payload.len + chunk_size - 1) / chunk_size
if total_frags == 0 {
header := [u8(`U`), `D`, `P`, `0`, 0, 1, 0, 0]
socket.write(header) or { return err }
return
}
for i in 0 .. total_frags {
start := i * chunk_size
mut end := (i + 1) * chunk_size
if end > payload.len {
end = payload.len
}
frag_len := end - start
mut packet := []u8{len: 8 + frag_len}
packet[0] = `U`
packet[1] = `D`
packet[2] = `P`
packet[3] = `0`
packet[4] = u8(i)
packet[5] = u8(total_frags)
packet[6] = u8((u32(frag_len) >> 8) & 0xff)
packet[7] = u8(u32(frag_len) & 0xff)
if frag_len > 0 {
unsafe {
C.memcpy(&packet[8], payload.str + start, frag_len)
}
}
socket.write(packet) or { return err }
// Sleep briefly to avoid packet loss during loopback transmission
time.sleep(2 * time.millisecond)
}
}
// write_udp_msg_to fragments and sends a message to a specific address using write_to.
// Real-world performance optimization: Avoids allocating a full payload copy by writing fragments
// directly from string memory slices using C.memcpy.
fn write_udp_msg_to(mut socket net.UdpConn, addr net.Addr, payload string) ! {
chunk_size := 1024
total_frags := (payload.len + chunk_size - 1) / chunk_size
if total_frags == 0 {
header := [u8(`U`), `D`, `P`, `0`, 0, 1, 0, 0]
socket.write_to(addr, header) or { return err }
return
}
for i in 0 .. total_frags {
start := i * chunk_size
mut end := (i + 1) * chunk_size
if end > payload.len {
end = payload.len
}
frag_len := end - start
mut packet := []u8{len: 8 + frag_len}
packet[0] = `U`
packet[1] = `D`
packet[2] = `P`
packet[3] = `0`
packet[4] = u8(i)
packet[5] = u8(total_frags)
packet[6] = u8((u32(frag_len) >> 8) & 0xff)
packet[7] = u8(u32(frag_len) & 0xff)
if frag_len > 0 {
unsafe {
C.memcpy(&packet[8], payload.str + start, frag_len)
}
}
socket.write_to(addr, packet) or { return err }
time.sleep(2 * time.millisecond)
}
}
// read_udp_msg reads packets from a socket and reassembles them into a single string.
// Security boundary: Filters out packets from unexpected addresses during reassembly,
// verifies index boundaries, and checks fragment total counts.
fn read_udp_msg(mut socket net.UdpConn, max_allowed_fragments int) !(string, net.Addr) {
mut fragments := map[int][]u8{}
mut total_frags := -1
mut remote_addr := net.Addr{}
mut buf := []u8{len: 2048}
for {
read, addr := socket.read(mut buf) or { return err }
if read == 0 {
return error('empty packet read')
}
if read < 8 {
return error('packet too small to contain header')
}
if buf[0] != `U` || buf[1] != `D` || buf[2] != `P` || buf[3] != `0` {
return error('invalid packet magic bytes')
}
frag_idx := int(buf[4])
total := int(buf[5])
frag_len := int((u32(buf[6]) << 8) | u32(buf[7]))
if read < 8 + frag_len {
return error('packet payload length mismatch')
}
if total > max_allowed_fragments {
return error('incoming message total fragments ${total} exceeds limit of ${max_allowed_fragments}')
}
if total <= 0 {
return error('invalid total fragments count')
}
if frag_idx < 0 || frag_idx >= total {
return error('invalid fragment index')
}
if total_frags == -1 {
total_frags = total
remote_addr = addr
} else {
// Injection defense: Ignore packets from other addresses during this reassembly
if addr.str() != remote_addr.str() {
continue
}
// Security validation: Mismatched fragment count from client mid-stream
if total != total_frags {
return error('fragment total count mismatch during reassembly')
}
}
fragments[frag_idx] = buf[8..8 + frag_len].clone()
if fragments.len == total_frags {
mut full_payload := []u8{}
for i in 0 .. total_frags {
if i !in fragments {
return error('missing fragment ${i} in reassembly')
}
full_payload << fragments[i]
}
return full_payload.bytestr(), remote_addr
}
}
return error('unexpected read loop termination')
}
// run_server starts the UDP server, processes fragments, reassembles them,
// and echoes back the full message or an error if size is exceeded.
fn run_server(port int) ! {
mut socket := net.listen_udp('127.0.0.1:${port}') or {
println('Server: Failed to listen on port ${port}: ${err}')
return err
}
defer {
socket.close() or {}
}
println('Server: Listening for UDP packets on port ${port}...')
mut reassemblers := map[string]UdpReassembler{}
mut buf := []u8{len: 2048}
for {
// Real-world security pruning: Sweeps stale reassembler states to prevent memory exhaustion DoS
now := time.now()
for key, state in reassemblers {
if now - state.last_seen > 5 * time.second {
reassemblers.delete(key)
}
}
read, addr := socket.read(mut buf) or {
println('Server: Read failed: ${err}')
break
}
if read == 0 {
break
}
if read < 8 {
println('Server: Received packet too small to contain header')
continue
}
// Verify header magic bytes
if buf[0] != `U` || buf[1] != `D` || buf[2] != `P` || buf[3] != `0` {
println('Server: Invalid packet magic bytes')
continue
}
frag_idx := int(buf[4])
total_frags := int(buf[5])
frag_len := int((u32(buf[6]) << 8) | u32(buf[7]))
if read < 8 + frag_len {
println('Server: Packet payload length mismatch')
continue
}
// Real-world safety limit check: Reject if fragment count exceeds threshold (max 5 fragments = 5KB)
max_allowed_fragments := 5
if total_frags > max_allowed_fragments {
println('Server: Rejected message from ${addr}. Total fragments ${total_frags} exceeds limit of ${max_allowed_fragments}.')
// Only send one error packet (on the first fragment index) to avoid flooding the client's socket queue
if frag_idx == 0 {
write_udp_msg_to(mut socket, addr, 'Error: Message size exceeds limit') or {}
}
continue
}
if total_frags <= 0 {
println('Server: Invalid total fragments count ${total_frags}')
continue
}
if frag_idx < 0 || frag_idx >= total_frags {
println('Server: Invalid fragment index ${frag_idx} for total ${total_frags}')
continue
}
addr_str := addr.str()
if addr_str !in reassemblers {
reassemblers[addr_str] = UdpReassembler{
total: total_frags
last_seen: now
}
}
mut r := reassemblers[addr_str]
// Reset state if fragment total count changes mid-stream
if r.total != total_frags {
println('Server: Resetting reassembler for ${addr} due to fragment total count change')
r = UdpReassembler{
total: total_frags
last_seen: now
}
}
r.last_seen = now
r.fragments[frag_idx] = buf[8..8 + frag_len].clone()
if r.fragments.len == r.total {
mut full_payload := []u8{}
mut success := true
for i in 0 .. r.total {
if i !in r.fragments {
success = false
break
}
full_payload << r.fragments[i]
}
// Clean up reassembler state
reassemblers.delete(addr_str)
if success {
message := full_payload.bytestr()
preview_len := if message.len > 30 { 30 } else { message.len }
println('Server received full message from ${addr} (len: ${message.len}): "${message[..preview_len]}"...')
if message == 'Goodbye' {
println('Server received Goodbye. Replying and exiting...')
write_udp_msg_to(mut socket, addr, 'Goodbye!') or {
println('Server: Write failed: ${err}')
}
break
}
response := 'Echo: ${message}'
write_udp_msg_to(mut socket, addr, response) or {
println('Server: Write failed: ${err}')
break
}
}
} else {
reassemblers[addr_str] = r
}
}
println('Server finished.')
}
// run_client connects to the UDP server and runs test cases (small, fragmented, overflow, goodbye).
fn run_client(port int) ! {
mut socket := net.dial_udp('127.0.0.1:${port}') or {
println('Client: Failed to dial server: ${err}')
return err
}
defer {
socket.close() or {}
}
println('Client: Bound to server destination.')
// 1. Send standard small message
msg1 := 'Ping 1'
println('Client sending small message: "${msg1}"')
write_udp_msg(mut socket, msg1)!
resp1, _ := read_udp_msg(mut socket, 5)!
println('Client received response: "${resp1}"')
time.sleep(50 * time.millisecond)
// 2. Send fragmented message within limit (3000 bytes -> 3 fragments)
msg2 := 'A'.repeat(3000)
println('Client sending fragmented message of length ${msg2.len} (3 fragments)...')
write_udp_msg(mut socket, msg2)!
resp2, _ := read_udp_msg(mut socket, 5)!
println('Client received response of length ${resp2.len} successfully!')
time.sleep(50 * time.millisecond)
// 3. Attempt to send message exceeding fragments limit (5500 bytes -> 6 fragments)
msg3 := 'B'.repeat(5500)
println('Client sending large message of length ${msg3.len} (6 fragments)...')
write_udp_msg(mut socket, msg3)!
resp3, _ := read_udp_msg(mut socket, 10)!
println('Client received response for overflow message: "${resp3}"')
time.sleep(50 * time.millisecond)
// 4. Send Goodbye to exit
println('Client sending: "Goodbye"')
write_udp_msg(mut socket, 'Goodbye')!
resp4, _ := read_udp_msg(mut socket, 5)!
println('Client received response: "${resp4}"')
}
fn main() {
println('=== Persistent UDP Protocol Demo ===')
port := 38294
// Spawn the server in a background thread
spawn fn (p int) {
run_server(p) or { println('Server thread failed: ${err}') }
}(port)
// Allow the server thread a short time to start and bind
time.sleep(100 * time.millisecond)
// Run the client in the main thread
run_client(port) or { println('Client failed: ${err}') }
// Give the server a small window to finish deferred cleanups
time.sleep(50 * time.millisecond)
println('UDP Protocol Demo finished.')
}Deep Dive Explanation: UDP Packet Fragmentation & Reassembly
1. UDP vs. TCP and the MTU Constraint
Unlike TCP, which handles packet streaming and division transparently, UDP is a datagram-oriented protocol. It sends individual, self-contained packets.
- Maximum Packet Sizes: The theoretical maximum size of a UDP packet is 65,535 bytes (including headers), but in practice, any packet larger than the network's MTU (Maximum Transmission Unit) (typically 1500 bytes on ethernet/internet routers) will be fragmented at the IP layer.
- Why IP Fragmentation is Bad: If any single IP fragment is lost during transmission, the entire UDP packet is discarded. This dramatically increases packet loss rates for large payloads.
- Solution: Implement application-level fragmentation. By splitting payloads into smaller chunks (e.g. 1024 bytes), we ensure each chunk fits comfortably inside a single MTU window, minimizing packet drops.
2. The Custom UDP Fragmentation Protocol
This example implements application-level fragmentation and reassembly using a custom 8-byte header prefix:
- Protocol ID (
UDP0): 4 bytes to identify valid application packets. - Fragment Index (1 byte): The sequence index of the current packet (0-indexed).
- Total Fragments (1 byte): The total number of packets that make up the complete message.
- Fragment Length (2 bytes): The size of the payload following this header (max 1024 bytes).
3. Reassembly Mechanics & State Management
Because UDP does not guarantee packet delivery order, packets can arrive out of sequence.
- State Tracking: The server uses a
reassemblersmap, keyed by the client's socket address (addr.str()). This maps each client to its ownUdpReassemblerstructure containing a map of packet indices to their payload bytes. - Sequence Ordering: Once the number of collected fragments matches
total_frags, the server iterates from0tototal_frags - 1to assemble the payload in the correct order, bypassing any network out-of-order delivery issues. - State Cleanup: As soon as a message is successfully reassembled,
reassemblers.delete(addr_str)is called. This frees memory immediately and prevents state leak.
4. Safety & DOS Protections
- Max Fragment Constraints: The server enforces
max_allowed_fragments := 5(equivalent to a maximum total message size of ~5 KB). If a packet arrives claiming a higher fragment count, it is discarded immediately to prevent malicious clients from exhausting server memory by flooding it with un-reassemblable data.
Net Unix
In plain words: Unix domain sockets (net.unix) let processes running on the same computer send messages through a local .sock file with ultra-low latency.
Unix Domain Sockets (UDS) provide a high-performance inter-process communication (IPC) channel on POSIX systems. Unlike standard TCP/UDP networking sockets which transmit data over the network stack (loopback interface), UDS transmits data directly inside the OS kernel, bypassing the IP protocol stack overhead completely. UDS endpoints are bound to filesystem paths (e.g., /tmp/mysocket). V's net.unix module provides listen_stream and connect functions that mimic standard TCP stream sockets, making it easy to build fast local microservices.
This example illustrates cleaning up stale socket files, launching a Unix domain socket listener/server, connecting a client to it, and exchanging messages.
This example demonstrates Unix domain socket client-server communication using the net.unix module.
module main
import net.unix
import os
import time
// run_server starts the Unix socket server, accepts one client connection,
// echoes back the received message, and exits.
fn run_server(socket_path string) ! {
// Clean up any stale socket file from a previous run
if os.exists(socket_path) {
os.rm(socket_path)!
}
// Listen on the Unix socket path with default options
mut listener := unix.listen_stream(socket_path, unix.ListenOptions{}) or {
println('Server: Failed to listen on ${socket_path}: ${err}')
return err
}
defer {
listener.close() or {}
listener.unlink() or {}
}
println('Server: Listening on socket path: ${socket_path}')
// Accept an incoming connection
mut conn := listener.accept() or {
println('Server: Failed to accept connection: ${err}')
return err
}
defer {
conn.close() or {}
}
println('Server: Client connected!')
// Read client's message
mut buf := []u8{len: 1024}
n := conn.read(mut buf) or {
println('Server: Read failed: ${err}')
return err
}
message := buf[..n].bytestr()
println('Server: Received message: "${message}"')
// Write response back to the client
response := 'Echo: ${message}'
conn.write(response.bytes()) or {
println('Server: Write failed: ${err}')
return err
}
println('Server: Sent echo response.')
}
// run_client connects to the Unix socket server, sends a message,
// reads the echo response, and closes the connection.
fn run_client(socket_path string) ! {
println('Client: Connecting to ${socket_path}...')
mut conn := unix.connect_stream(socket_path) or {
println('Client: Failed to connect: ${err}')
return err
}
defer {
conn.close() or {}
}
println('Client: Connected!')
// Send message to the server
message := 'Hello V Unix Domain Sockets!'
println('Client: Sending message: "${message}"')
conn.write(message.bytes()) or {
println('Client: Write failed: ${err}')
return err
}
// Read server response
mut buf := []u8{len: 1024}
n := conn.read(mut buf) or {
println('Client: Read failed: ${err}')
return err
}
response := buf[..n].bytestr()
println('Client: Received response: "${response}"')
}
fn main() {
println('=== net.unix Module Demo ===')
// Create a unique temporary socket path
socket_path := os.join_path(os.temp_dir(), 'v_unix_socket_example')
// Spawn the server in a background thread
spawn fn (path string) {
run_server(path) or { println('Server thread failed: ${err}') }
}(socket_path)
// Allow the server thread a short time to start and bind
time.sleep(100 * time.millisecond)
// Run the client in the main thread
run_client(socket_path) or { println('Client failed: ${err}') }
// Give the server a small window to finish deferred cleanups
time.sleep(50 * time.millisecond)
println('Unix Sockets Demo finished.')
}Unix Persistent
In plain words: Builds a stateful local IPC server using Unix domain sockets so two programs on the same machine can talk at memory speed.
This example demonstrates a persistent length-prefixed Unix domain socket connection, processing payloads in chunks, and rejecting messages exceeding safety size boundaries.
What happens in this example:
- Code note: write_msg sends a message using a 4-byte magic signature and a 4-byte big-endian length in a single write syscall.
- Code note: Send consolidated buffer in a single system call
- Code note: read_exact reads exactly
sizebytes from the connection, processing data in chunks. - Code note: Real-world performance optimization: Reads directly into mutable slice views of our pre-allocated
module main
import net.unix
import os
import time
const max_message_size = 8192
// write_msg sends a message using a 4-byte magic signature and a 4-byte big-endian length in a single write syscall.
fn write_msg(mut conn unix.StreamConn, payload string) ! {
mut buf := []u8{len: 8 + payload.len}
buf[0] = `M`
buf[1] = `S`
buf[2] = `G`
buf[3] = `0`
buf[4] = u8((u32(payload.len) >> 24) & 0xff)
buf[5] = u8((u32(payload.len) >> 16) & 0xff)
buf[6] = u8((u32(payload.len) >> 8) & 0xff)
buf[7] = u8(u32(payload.len) & 0xff)
if payload.len > 0 {
unsafe {
C.memcpy(&buf[8], payload.str, payload.len)
}
}
// Send consolidated buffer in a single system call
conn.write(buf) or { return err }
}
// read_exact reads exactly `size` bytes from the connection, processing data in chunks.
// Real-world performance optimization: Reads directly into mutable slice views of our pre-allocated
// buffer to achieve zero-allocation reads inside the chunking loop.
fn read_exact(mut conn unix.StreamConn, size int) ![]u8 {
mut data := []u8{len: size}
mut read_bytes := 0
for read_bytes < size {
remaining := size - read_bytes
// Use a small buffer chunk limit (e.g. 512 bytes) to demonstrate reading in chunks
chunk_limit := if remaining > 512 { 512 } else { remaining }
n := conn.read(mut data[read_bytes..read_bytes + chunk_limit]) or { return err }
if n == 0 {
if read_bytes == 0 {
return error('EOF')
}
return error('unexpected end of stream')
}
read_bytes += n
}
return data
}
// read_msg reads a single framed message.
fn read_msg(mut conn unix.StreamConn, max_size int) !string {
// Read header: 4 magic bytes + 4 length bytes = 8 bytes
header_bytes := read_exact(mut conn, 8) or { return err }
// Validate protocol magic bytes
if header_bytes[0] != `M` || header_bytes[1] != `S` || header_bytes[2] != `G`
|| header_bytes[3] != `0` {
return error('invalid protocol magic bytes')
}
// Reconstruct big-endian length
len := int((u32(header_bytes[4]) << 24) | (u32(header_bytes[5]) << 16) | (u32(header_bytes[6]) << 8) | u32(header_bytes[7]))
// Real-world security boundary: Reject messages larger than allowed limit to prevent DoS (OOM)
if len > max_size {
return error('message size ${len} exceeds limit of ${max_size} bytes')
}
if len < 0 {
return error('invalid negative message length')
}
// Read the actual payload
payload_bytes := read_exact(mut conn, len) or { return err }
return payload_bytes.bytestr()
}
// run_server starts the Unix socket server, accepts a connection,
// and processes incoming messages in a loop according to our framing protocol.
fn run_server(socket_path string) ! {
// Clean up any stale socket file from a previous run
if os.exists(socket_path) {
os.rm(socket_path)!
}
// Listen on the Unix socket path
mut listener := unix.listen_stream(socket_path, unix.ListenOptions{}) or {
println('Server: Failed to listen on ${socket_path}: ${err}')
return err
}
defer {
listener.close() or {}
listener.unlink() or {}
}
println('Server: Listening on socket path: ${socket_path}')
mut conn := listener.accept() or {
println('Server: Failed to accept connection: ${err}')
return err
}
defer {
conn.close() or {}
}
println('Server: Client connected!')
// Real-world safety practice: Set read and write timeouts to prevent connection hang-ups (Slowloris DoS)
conn.set_read_timeout(time.second * 5)
conn.set_write_timeout(time.second * 5)
for {
message := read_msg(mut conn, max_message_size) or {
if err.msg() == 'EOF' {
println('Server: Client disconnected cleanly (EOF).')
} else {
println('Server: Connection closed or protocol error: ${err}')
}
break
}
// Preview message content
preview_len := if message.len > 30 { 30 } else { message.len }
println('Server received message (len: ${message.len}): "${message[..preview_len]}"...')
if message == 'Goodbye' {
println('Server received Goodbye. Replying and closing connection...')
write_msg(mut conn, 'Goodbye!') or { println('Server: Write failed: ${err}') }
break
}
response := 'Echo: ${message}'
write_msg(mut conn, response) or {
println('Server: Write failed: ${err}')
break
}
}
println('Server finished.')
}
// run_client connects to the Unix socket server, sends multiple messages (including
// a large chunked message and an invalid/overflow message), and validates responses.
fn run_client(socket_path string) ! {
println('Client: Connecting to ${socket_path}...')
mut conn := unix.connect_stream(socket_path) or {
println('Client: Failed to connect: ${err}')
return err
}
defer {
conn.close() or {}
}
println('Client: Connected!')
// Set connection timeouts for the client too
conn.set_read_timeout(time.second * 5)
conn.set_write_timeout(time.second * 5)
// 1. Send a standard small message
msg1 := 'Ping 1'
println('Client sending small message: "${msg1}"')
write_msg(mut conn, msg1)!
resp1 := read_msg(mut conn, max_message_size)!
println('Client received response: "${resp1}"')
time.sleep(50 * time.millisecond)
// 2. Send a large message within limit (5000 bytes) to trigger chunked read assembly
msg2 := 'A'.repeat(5000)
println('Client sending large message of length ${msg2.len}...')
write_msg(mut conn, msg2)!
resp2 := read_msg(mut conn, max_message_size)!
println('Client received response of length ${resp2.len} successfully!')
time.sleep(50 * time.millisecond)
// 3. Attempt to send an invalid/overflow message (header length > max_message_size)
println('Client sending invalid header claiming 100,000 bytes payload...')
magic := [u8(`M`), `S`, `G`, `0`]
bad_len_bytes := [u8(0), 1, 134, 160] // 100,000 big-endian
conn.write(magic)!
conn.write(bad_len_bytes)!
// The server must reject the message and terminate the connection
mut buf := []u8{len: 1}
n := conn.read(mut buf) or {
println('Client: Successfully verified server rejected overflow and closed connection: ${err}')
return
}
if n == 0 {
println('Client: Successfully verified server rejected overflow (EOF received).')
} else {
println('Client: Warning - Server did not close connection on overflow!')
}
}
fn main() {
println('=== Persistent Unix Sockets Protocol Demo ===')
socket_path := os.join_path(os.temp_dir(), 'v_unix_socket_persistent')
// Spawn the server in a background thread
spawn fn (path string) {
run_server(path) or { println('Server thread failed: ${err}') }
}(socket_path)
// Allow the server thread a short time to start and bind
time.sleep(100 * time.millisecond)
// Run the client in the main thread
run_client(socket_path) or { println('Client failed: ${err}') }
// Give the server a small window to finish deferred cleanups
time.sleep(50 * time.millisecond)
println('Unix Sockets Protocol Demo finished.')
}Other Stdlib Updates
This section is grouped into focused subtopics so you can jump quickly to the area you need. The examples here were expanded from the runnable standard-library demos under the repository's languageupdatesandstdlib/02standard_library folder and verified by running them with V.
Core Language and Type Features
Standard Library and OS Modules
- Strings Builder
- Os Advanced Io
- Os Operations
- Os Process Pipe
- Os System Info
- Time And Stopwatch
- Http Client
- Regex Matching
- Command Line Flags
- Datatypes Collections
- Gg Graphics
- Command Line Arguments
- Math And Rand
Security, Data, and Formats
- Crypto Asymmetric
- Crypto Entropy
- Crypto Hash
- Crypto Kdf
- Crypto Mac
- Crypto Symmetric
- Log And Crypto
- Encoding Formats
- Arrays Utility
- Toml
- Strconv
- Semver
- Maps Standard Library Module (maps.v)
- Archive Tar
- Compress Deflate
- Compress Gzip
- Compress Szip
- Compress Zlib
- Compress Zstd
- Hash
- Bitfield
Concurrency, CLI, and App Development
I/O, Streams, and Terminal
Options And Results
In plain words: Demonstrates the difference between an Option (?T) (used when a lookup might simply find none) and a Result (!T) (used when an operation might fail with an error(...)), and how or { ... } handles both safely.
What happens in this example:
- Code note: Result type (!T) is used when a function can return an error.
- Code note: Option type (?T) is used when a function can return nothing (none).
- Code note: 1. Handling a Result type with an
orblock - Code note: Inside the
orblock, the special variableerris available.
module main
// Result type (!T) is used when a function can return an error.
fn divide(a f64, b f64) !f64 {
if b == 0 {
return error('division by zero')
}
return a / b
}
// Option type (?T) is used when a function can return nothing (none).
fn find_user(id int) ?string {
if id == 1 {
return 'Alice'
}
return none
}
fn main() {
// 1. Handling a Result type with an `or` block
// Inside the `or` block, the special variable `err` is available.
val1 := divide(10.0, 2.0) or {
println('Error: ${err}')
0.0
}
println('Result 1: ${val1}')
// 2. Handling a failed Result
val2 := divide(10.0, 0.0) or {
println('Error: ${err}')
0.0
}
println('Result 2: ${val2}')
// 3. Handling an Option type with an `or` block
// For Option types, the value is unwrapped or the fallback value is returned.
user1 := find_user(1) or { 'Guest' }
println('User 1: ${user1}')
user2 := find_user(99) or { 'Guest' }
println('User 2: ${user2}')
// 4. Using if-let syntax to check Options
if name := find_user(1) {
println('Found user: ${name}')
} else {
println('User not found')
}
}Generics
In plain words: Generics ([T]) let you write a struct (like Stack[T]) or function (print_val[T]) once as a flexible template that works with int, string, or any other type!
What happens in this example:
- Code note: Stack[T] represents a generic stack structure.
- Code note: push appends an item of type T to the stack.
- Code note: pop removes and returns the top item of type T from the stack,
- Code note: or returns
none(Option type) if the stack is empty.
module main
// Stack[T] represents a generic stack structure.
struct Stack[T] {
mut:
items []T
}
// push appends an item of type T to the stack.
fn (mut s Stack[T]) push(item T) {
s.items << item
}
// pop removes and returns the top item of type T from the stack,
// or returns `none` (Option type) if the stack is empty.
fn (mut s Stack[T]) pop() ?T {
if s.items.len == 0 {
return none
}
return s.items.pop()
}
// print_val is a generic function that takes any type T and prints it.
fn print_val[T](val T) {
println('Value: ${val}')
}
fn main() {
// 1. Using a generic struct with integers
mut int_stack := Stack[int]{}
int_stack.push(10)
int_stack.push(20)
println('Popped: ${int_stack.pop() or { 0 }}')
println('Popped: ${int_stack.pop() or { 0 }}')
println('Popped from empty stack: ${int_stack.pop() or { -1 }}')
// 2. Using the same generic struct with strings
mut str_stack := Stack[string]{}
str_stack.push('V')
str_stack.push('lang')
println('Popped: ${str_stack.pop() or { 'empty' }}')
println('Popped: ${str_stack.pop() or { 'empty' }}')
// 3. Calling a generic function with different types
print_val[string]('V monomorphizes generics at compile-time!')
print_val[f64](3.14159)
}Interfaces
In plain words: An interface defines a shared ability (like speak()). Any struct (such as Dog or Cat) that has a matching speak() method can be used wherever that interface is expected.
What happens in this example:
- Code note: Speaker is an interface. Any struct that implements a
speak() stringmethod - Code note: implicitly implements Speaker. There is no
implementskeyword. - Code note: speak implements Speaker for Dog
- Code note: speak implements Speaker for Cat
module main
// Speaker is an interface. Any struct that implements a `speak() string` method
// implicitly implements Speaker. There is no `implements` keyword.
interface Speaker {
speak() string
}
struct Dog {
name string
}
// speak implements Speaker for Dog
fn (d Dog) speak() string {
return 'Woof! My name is ${d.name}.'
}
struct Cat {
name string
}
// speak implements Speaker for Cat
fn (c Cat) speak() string {
return 'Meow! My name is ${c.name}.'
}
// perform_speak accepts any type implementing the Speaker interface
fn perform_speak(s Speaker) {
println(s.speak())
}
fn main() {
d := Dog{
name: 'Buddy'
}
c := Cat{
name: 'Whiskers'
}
// 1. Passing structs directly to functions expecting an interface
perform_speak(d)
perform_speak(c)
// 2. Creating an array of interfaces
speakers := [Speaker(d), Speaker(c)]
for speaker in speakers {
println('From array: ${speaker.speak()}')
}
}Sum Types
In plain words: A Sum Type (type Shape = Circle | Rectangle) lets a single variable hold one of several listed types safely, using match or if x is Circle to check which one is inside.
What happens in this example:
- Code note: Define structs for different shapes
- Code note: Shape is a Sum Type. A Shape variable can store a Circle, Rectangle, or Triangle.
- Code note: get_area calculates the area depending on the concrete type stored in Shape.
- Code note: Inside the match branches, the variable is smart-casted to its concrete type.
module main
// Define structs for different shapes
struct Circle {
radius f64
}
struct Rectangle {
width f64
height f64
}
struct Triangle {
base f64
height f64
}
// Shape is a Sum Type. A Shape variable can store a Circle, Rectangle, or Triangle.
type Shape = Circle | Rectangle | Triangle
// get_area calculates the area depending on the concrete type stored in Shape.
fn get_area(s Shape) f64 {
// Inside the match branches, the variable is smart-casted to its concrete type.
match s {
Circle {
return 3.14159 * s.radius * s.radius
}
Rectangle {
return s.width * s.height
}
Triangle {
return 0.5 * s.base * s.height
}
}
}
fn main() {
// 1. Creating values of the sum type
shapes := [
Shape(Circle{
radius: 5.0
}),
Shape(Rectangle{
width: 4.0
height: 6.0
}),
Shape(Triangle{
base: 3.0
height: 4.0
}),
]
// 2. Iterating and pattern-matching
for shape in shapes {
match shape {
Circle {
println('Found Circle with radius ${shape.radius}. Area: ${get_area(shape):.2f}')
}
Rectangle {
println('Found Rectangle of ${shape.width}x${shape.height}. Area: ${get_area(shape):.2f}')
}
Triangle {
println('Found Triangle with base ${shape.base} and height ${shape.height}. Area: ${get_area(shape):.2f}')
}
}
}
}Attributes
In plain words: Attributes (@[...]) are special tags you place above functions or structs (like @[deprecated], @[inline], or @[json: 'name']) to give extra instructions to the V compiler.
What happens in this example:
- Code note: User uses attributes to control JSON field names and to hide a field from encoding.
- Code note: Note shows how database-related attributes can describe a schema shape.
- Code note: deprecated warns developers when they call this function.
- Code note: moderngreet is the preferred replacement for oldgreet.
module main
import json2
// User uses attributes to control JSON field names and to hide a field from encoding.
struct User {
name string @[json: 'username']
age int @[json: 'user_age']
secret string @[json: '-']
}
// Note shows how database-related attributes can describe a schema shape.
struct Note {
id int @[primary; sql: serial]
message string @[sql: 'detail'; unique]
}
// deprecated warns developers when they call this function.
@[deprecated: 'use modern_greet instead']
fn old_greet() {
println('Hello from the old greeting!')
}
// modern_greet is the preferred replacement for old_greet.
fn modern_greet() {
println('Hello from the modern greeting!')
}
// inline hints the compiler that this small function should be inlined.
@[inline]
fn add(a int, b int) int {
return a + b
}
// required marks a function parameter as something that should be supplied explicitly.
@[required]
fn greet_user(name string) string {
return 'Hello, ${name}!'
}
fn main() {
println('=== attributes demo ===')
// Build a User instance and encode it to JSON.
u := User{
name: 'Bob'
age: 30
secret: 'hidden'
}
encoded := json2.encode(u)
println('Encoded JSON: ${encoded}')
// Decode a JSON payload that uses the custom field names from the attributes.
decoded := json2.decode[User]('{"username":"Alice","user_age":25}') or {
println('JSON error: ${err}')
User{}
}
println('Decoded User -> Name: ${decoded.name}, Age: ${decoded.age}')
// The inline attribute is only a hint, but the example shows the function call.
sum := add(10, 20)
println('Sum: ${sum}')
// Call the modern function and the required-parameter helper.
modern_greet()
println(greet_user('Ada'))
// The Note struct is only used to demonstrate the attribute syntax here.
println('Note schema fields: ${Note{}.id} / ${Note{}.message}')
// Calling old_greet() will compile successfully but output a warning:
// warning: old_greet has been deprecated. use modern_greet instead
// old_greet()
}Compile-Time Directives & Compile-Time Code
In plain words: Compile-time directives ($if windows, $if macos, $if debug, $for, $embed_file) let V customize or include code automatically based on the operating system or build mode.
V provides a powerful set of compile-time (or 'comptime') directives and code features, prefixed with $. These instructions are evaluated and processed by the compiler during compilation, allowing you to optimize code execution, prune unused branches, dynamically query compilation environment properties, and embed assets directly into the final binary.
1. Conditional Compilation (`$if` Condition)
If you want an if expression to be evaluated at compile time, prefix it with $. Inactive branches are excluded from compilation entirely, meaning their type checks still occur but no code is generated for them in the final executable.
- Multiple Conditions: You can combine multiple platforms or build modes in one branch using logic operators (
||,&&). - Expression Usage: A compile-time
$ifcan be used as an expression to conditionally assign values. $else-$ifChains: You can chain compile-time conditions using$else $ifto check against various compilers, platforms, or custom defines.
Builtin `$if` Compilation Target Options
Below is the full list of builtin options supported inside compile-time $if conditions:
| OS target | Compilers | Platforms | Other |
|---|---|---|---|
windows, linux, macos | gcc, tinyc | amd64, arm64, aarch64 | debug, prod, test |
darwin, ios, bsd | clang, mingw | i386, arm32 | js, glibc, prealloc |
freebsd, openbsd, netbsd | msvc | rv64, rv32, s390x | no_bounds_checking, freestanding |
android, mach, dragonfly | cplusplus | ppc64le | no_segfault_handler, no_backtrace |
gnu, hpux, haiku, qnx | x64, x32 | no_main, fast_math, apk, threads | |
solaris, termux | little_endian, big_endian | js_node, js_browser, js_freestanding | |
serenity, vinix, plan9 | interpreter, es5, profile, wasm32 | ||
wasm32_emscripten, wasm32_wasi, native, autofree |
2. Compile-Time Flag Defines (`$d`)
V allows retrieving custom flag values defined via the command line with -d flag=value or -d flag (which defaults to -d flag=true).
- To fetch the flag inside your code, use:
$d('flag_name', default_value). - The
default_valueacts as a fallback when the flag is not provided on the command line. It must be a pure literal: booleans (true/false), integers (0), floats (0.0), strings ('string'), or runes (\v\``). - You can also use
$d('flag_name', false)inside$ifconditions (e.g.$if $d('my_flag', false) { ... }) to selectively enable or disable blocks of code. $dcan also be used in top-level statements like#flagand#include(e.g.,#flag linux -I $d('my_include', '/usr')/include).
3. Compile-Time Warnings & Errors
You can generate custom compile-time messages to warn the developer or abort the build:
$compile_warn('message')prints a warning during compilation but allows the build to continue.$compile_error('message')immediately halts compilation and prints a custom error.
These are particularly powerful when combined with platform target checks to enforce compatibility (e.g., aborting compilation on unsupported architectures).
4. Environment Variables (`$env`)
$env('VAR_NAME') retrieves the value of an environment variable at compilation time and embeds it as a string literal. It can also be used inside top-level #flag and #include statements.
5. File Asset Embedding (`$embed_file`)
V can embed the raw content of any external file directly inside the compiled binary using $embed_file('path').
- Returns an
EmbedFileDatastructure. Use.to_string()or.to_bytes()to retrieve contents. - In production builds (
-prod),$embed_filesupports optional on-the-fly compression via.zlib(e.g.$embed_file('x.css', .zlib)). - For local development ease, compile with
-d embed_only_metadata. The file won't be embedded, and V will load the file from disk the first timedata()is called, permitting external live edits without recompiling.
6. Compile-Time Templates (`$tmpl`)
$tmpl('path/to/template.html') compiles and parses a simple template file, interpolating any variables (prefixed with @ in the template) that exist in the calling scope.
Here is a comprehensive code example highlighting all compile-time directives and code features in action:
module main
fn main() {
println('=== V Compile-Time Directives & Code Demo ===')
// 1. Conditional Compilation ($if) and multiple conditions
println('\n--- 1. Conditional Compilation (compile-time \$if) ---')
$if macos {
println('OS target: macOS')
}
$if windows {
println('OS target: Windows')
}
$if linux {
println('OS target: Linux')
}
// Multiple conditions in one branch
$if ios || android {
println('Target platform is a mobile device (iOS/Android).')
} $else $if macos || linux || windows {
println('Target platform is a desktop OS.')
}
$if linux && x64 {
println('Running specifically on 64-bit Linux.')
}
// 2. $if as an expression
println('\n--- 2. \$if Used as an Expression ---')
os_family := $if windows { 'Windows' } $else { 'Unix-like' }
println('OS Family expression: ${os_family}')
// 3. $else-$if compiler branches
println('\n--- 3. Compiler Type Detection (\$else-\$if) ---')
$if tinyc {
println('Compiled with: TinyC')
} $else $if clang {
println('Compiled with: Clang')
} $else $if gcc {
println('Compiled with: GCC')
} $else $if msvc {
println('Compiled with: MSVC')
} $else {
println('Compiled with a different/unspecified compiler')
}
// 4. Custom Compile-time Flag defines ($d) with defaults
println('\n--- 4. Compile-Time Flags (\$d) with Default Values ---')
// $d brings values defined via compiler flags (-d flag=val or -d flag)
// Default value must be a pure literal (boolean, int, float, string, or rune)
custom_str := $d('custom_str', 'Default Text')
custom_bool := $d('custom_bool', false)
custom_int := $d('custom_int', 42)
custom_float := $d('custom_float', 3.14159)
custom_char := $d('custom_char', `v`)
println('custom_str: ${custom_str}')
println('custom_bool: ${custom_bool}')
println('custom_int: ${custom_int}')
println('custom_float: ${custom_float}')
println('custom_char: ${rune(custom_char)}')
// We can also use $d('ident', false) inside $if condition to conditionally enable/disable code:
$if $d('enable_feature', false) {
println('Special feature is ENABLED at compile-time!')
} $else {
println('Special feature is DISABLED (default). Compile with `v -d enable_feature run directives.v` to enable.')
}
// 5. Compile-time custom errors and warnings
println('\n--- 5. Compile-Time Errors and Warnings (\$compile_error, \$compile_warn) ---')
// These only trigger if the enclosing $if branch is active/evaluated at compile time.
$if $d('trigger_error', false) {
$compile_error('Explicit compile-time error triggered')
}
$if $d('trigger_warn', false) {
$compile_warn('Explicit compile-time warning triggered')
}
println('No compile-time errors/warnings triggered during this compilation run.')
// 6. $env reads environment values while the program is being compiled.
println('\n--- 6. Compile-Time Environment (compile-time env) ---')
compile_path := $env('PATH')
println('PATH length at compile-time: ${compile_path.len} bytes')
// 7. $embed_file stores a file's contents inside the compiled binary.
println('\n--- 7. Asset Embedding (compile-time embed) ---')
embedded_file := $embed_file('temp_embed.txt')
content := embedded_file.to_string()
println('Embedded File Content:')
println(content)
// 8. $tmpl renders a template file and injects the current variables.
println('\n--- 8. Template Interpolation (compile-time template) ---')
name := 'Developer'
status := 'active'
rendered_template := $tmpl('template.html')
println('Rendered Template Output:')
println(rendered_template)
}Strings Builder
In plain words: When gluing hundreds of pieces of text together in a loop, strings.new_builder(...) acts like a fast notepad buffer so V doesn't have to copy the whole string on every step.
What happens in this example:
- Code note: 1. Initialize a new Builder with pre-allocated buffer size (e.g. 100 bytes).
- Code note: Pre-allocation is highly recommended for performance to reduce memory allocations.
- Code note: 2. Write strings and runes to the buffer
- Code note: 3. Extract the final constructed string
module main
import strings
fn main() {
// 1. Initialize a new Builder with pre-allocated buffer size (e.g. 100 bytes).
// Pre-allocation is highly recommended for performance to reduce memory allocations.
mut sb := strings.new_builder(100)
// 2. Write strings and runes to the buffer
sb.write_string('Welcome ')
sb.write_string('to ')
sb.write_string('the V standard library!')
sb.write_rune(`\n`)
sb.write_string('V is:\n')
features := ['Fast', 'Simple', 'Statically Typed', 'Safe']
for feature in features {
sb.write_string('- ')
sb.write_string(feature)
sb.write_rune(`\n`)
}
// 3. Extract the final constructed string
result := sb.str()
println(result)
// 4. Reset/Clear the builder to reuse it
// In V, `clear()` clears the builder's buffer.
sb.clear()
sb.write_string('New content in builder.')
println(sb.str())
}Os Advanced Io
In plain words: Covers advanced os file tools: walking directory trees, checking file permissions/sizes (os.stat), reading files line-by-line, and working with environment variables.
This example demonstrates advanced Unix file behaviors such as raw struct binary serialization, cursor seeking (seek/tell), file size truncation, and recursive directory tree traversal.
module main
import os
struct Config {
mut:
id int
val f64
name [20]u8 // fixed-size array of bytes for safe serialization
}
fn main() {
println('=== V Advanced File I/O & Directory Walking ===')
file_path := 'temp_advanced_io.bin'
// --- 1. Struct Reading & Writing (Binary Serialization) ---
println('\n--- 1. Struct Binary Serialization ---')
// Create a mutable file in write/read mode
mut f := os.open_file(file_path, 'w+') or {
println('Failed to open file: ${err}')
return
}
mut cfg := Config{
id: 101
val: 99.99
name: [u8(0), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]!
}
// Populate name
name_str := 'V-OS-Advanced-IO'
for i in 0 .. name_str.len {
if i < 20 {
cfg.name[i] = name_str[i]
}
}
// Write struct representation directly to the file
f.write_struct(cfg) or { println('Failed to write struct: ${err}') }
println('Struct successfully serialized to file.')
// --- 2. Seeking & Cursor Position (seek/tell) ---
println('\n--- 2. File Seeking & Cursor Position ---')
// Retrieve current position in the file (should be size of struct)
pos := f.tell() or { 0 }
println('Current file cursor position: ${pos} bytes')
// Seek back to the beginning of the file (.start)
println('Seeking back to the start of the file...')
f.seek(0, .start) or { println('Failed to seek: ${err}') }
// Read struct back from file
mut read_cfg := Config{
name: [u8(0), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]!
}
f.read_struct(mut read_cfg) or { println('Failed to read struct: ${err}') }
// Extract string from fixed-size byte array
mut bytes := []u8{}
for b in read_cfg.name {
if b == 0 {
break
}
bytes << b
}
name_read := bytes.bytestr()
println('Deserialized Struct:')
println(' ID: ${read_cfg.id}')
println(' Val: ${read_cfg.val}')
println(' Name: ${name_read}')
f.close()
// --- 3. Truncating Files ---
println('\n--- 3. File Truncation (truncate) ---')
// Note: V's os.truncate opens the file with O_TRUNC, resetting it first before sizing.
// Shrinking/sizing a file directly using os.truncate:
println('Truncating file "${file_path}" to 10 bytes...')
os.truncate(file_path, 10) or { println('Failed to truncate: ${err}') }
println('File size after truncation: ${os.file_size(file_path)} bytes')
// Clean up binary file
os.rm(file_path) or {}
// --- 4. Recursive Directory Tree Walking ---
println('\n--- 4. Directory Tree Walking (walk) ---')
// Create a dummy tree for traversal
walk_root := 'temp_walk_root'
sub_dir := os.join_path(walk_root, 'docs')
os.mkdir_all(sub_dir) or {}
os.write_file(os.join_path(walk_root, 'file1.txt'), 'content1') or {}
os.write_file(os.join_path(sub_dir, 'file2.log'), 'content2') or {}
os.write_file(os.join_path(sub_dir, 'file3.txt'), 'content3') or {}
// Recursive walk using a callback
println('Recursive walk using os.walk (all files):')
os.walk(walk_root, fn (path string) {
println(' Found file: ${path}')
})
// Walk with file extension filter
println('Walk with file extension filter using os.walk_ext (.txt only):')
txt_files := os.walk_ext(walk_root, '.txt', os.WalkParams{})
for path in txt_files {
println(' Found .txt file: ${path}')
}
// Cleanup directory tree
os.rmdir_all(walk_root) or {}
println('Directory tree cleanup complete.')
}Os Operations
In plain words: Shows the everyday os essentials: creating folders, writing text to a file (os.write_file), reading it back (os.read_file), listing files (os.ls), and deleting temporary files.
V's standard library provides a rich set of cross-platform functions for interacting with the operating system through the os module. Here is the simplest, most practical guide to when you should actually use each of these functions in real-world programming.
1. Basic File Operations
- The Vibe: The "Standard file cabinets."
- What it does: Writes, reads, and checks the existence of files using simple string and byte helpers.
- Best to use when: You need to dump text, save config settings, or read small files quickly.
- Real-world example: Writing user session logs or reading a local settings file.
2. Directory Tree Operations
- The Vibe: The "Digital folder builder."
- What it does: Creates nested folders (
mkdir_all) or removes them (rmdir_all). - Best to use when: You need to construct file paths for organized data storage.
- Real-world example: Creating a new user cache folder like
cache/images/temp/.
3. Path Manipulation & Extraction
- The Vibe: The "Path dissection tool."
- What it does: Extracts directory paths, base names, file extensions, and normalizes them.
- Best to use when: You have a file path and want to rename the file or get its extension without manual string parsing.
- Real-world example: Checking if an uploaded file has a
.jpgextension.
4. Environment & Command Execution
- The Vibe: "Talking to the host machine."
- What it does: Reads system environment variables and runs shell commands.
- Best to use when: You need to fetch config keys (like API tokens) or execute external utilities (like running
git version). - Real-world example: Fetching the
HOMEdirectory to locate user configuration files.
5. File Permissions & Ownership (Chmod/Chown)
- The Vibe: The "Keymaster/security guard."
- What it does: Alters who can read/write/execute a file and changes user/group owner IDs.
- Best to use when: Making a script executable or securing sensitive credentials.
- Real-world example: Restricting a database file's permissions to be readable only by the owner (
chmod 0o600).
6. Globbing
- The Vibe: The "Wildcard detector."
- What it does: Matches a list of files using wildcard patterns (like
*.txt). - Best to use when: You need to process a batch of files matching a name template.
- Real-world example: Storing files matching
log_*.txtand deleting them in a batch.
module main
import os
fn main() {
filename := 'temp_book_example.txt'
content := 'V standard library makes OS operations very straightforward.'
// ==========================================
// 1. Basic File Operations (Writing, Reading, Existence)
// ==========================================
// os.write_file writes a string to a file. It overwrites the file if it already exists.
// We handle errors using V's explicit "or" block.
println('Writing text to ${filename}...')
os.write_file(filename, content) or {
println('Failed to write file: ${err}')
return
}
// os.exists checks if a file or directory exists at the given path.
if os.exists(filename) {
println('Confirmed: File exists.')
}
// os.read_file reads the entire content of a file and returns it as a string.
read_content := os.read_file(filename) or {
println('Failed to read file: ${err}')
return
}
println('Read content from file: "${read_content}"')
// os.write_lines writes an array of strings to a file, separating them with newlines.
lines := ['Line 1: V has simple OS functions.', 'Line 2: Supporting multiple lines.']
lines_file := 'temp_lines_example.txt'
os.write_lines(lines_file, lines) or { println('Failed to write lines: ${err}') }
// os.read_lines reads a file line-by-line and returns an array of strings.
read_lines := os.read_lines(lines_file) or {
println('Failed to read lines: ${err}')
[]
}
println('Read lines: ${read_lines}')
os.rm(lines_file) or {}
// os.write_bytes and os.read_bytes handle raw binary byte arrays.
// os.file_last_mod_unix retrieves the Unix timestamp of when the file was last modified.
// os.is_file returns true if the path points to a file (not a directory).
bytes_file := 'temp_bytes_example.bin'
os.write_bytes(bytes_file, 'V handles raw bytes.'.bytes()) or {
println('Failed to write bytes: ${err}')
}
read_bytes := os.read_bytes(bytes_file) or { []u8{} }
println('Read bytes: "${read_bytes.bytestr()}"')
println('Last modified time (epoch): ${os.file_last_mod_unix(bytes_file)}')
println('Is a file? ${os.is_file(bytes_file)}')
os.rm(bytes_file) or {}
// os.create creates a new empty file for writing and returns a File handle.
// os.open_append opens an existing file or creates one, positioning the cursor at the end to append data.
// os.open opens an existing file in read-only mode.
handle_file := 'temp_handle_example.txt'
mut f_create := os.create(handle_file) or { panic(err) }
f_create.write_string('Line 1 from file handle\n') or {}
f_create.close()
mut f_append := os.open_append(handle_file) or { panic(err) }
f_append.write_string('Line 2 appended\n') or {}
f_append.close()
mut f_read := os.open(handle_file) or { panic(err) }
mut buf := []u8{len: 100}
n_read := f_read.read(mut buf) or { 0 }
println('Content via file handle:\n${buf[..n_read].bytestr().trim_space()}')
f_read.close()
os.rm(handle_file) or {}
// os.ls returns a list of file and directory names inside the target directory path.
println('Listing files in current directory:')
files := os.ls('.') or {
println('Failed to list directory: ${err}')
[]
}
for file in files {
if file == filename {
println('- Found file: ${file}')
}
}
// os.getenv retrieves the value of a system environment variable.
home_dir := os.getenv('HOME')
println('User HOME directory: ${home_dir}')
// os.exists_in_system_path checks if a command binary is present in the system's PATH.
if os.exists_in_system_path('git') {
println('Confirmed: Git executable exists in system PATH.')
}
// os.execute runs a system command in a shell and returns a Result struct.
// The Result contains both the command exit_code and stdout/stderr output.
println('Running command "uname"...')
res := os.execute('uname')
if res.exit_code == 0 {
println('Operating System: ${res.output.trim_space()}')
} else {
println('Command execution failed with code ${res.exit_code}: ${res.output}')
}
// ==========================================
// 2. Directory Tree Operations
// ==========================================
println('\n--- Directory Tree Operations ---')
// os.mkdir_all recursively creates a full nested directory path (similar to mkdir -p).
nested_dir := os.join_path('temp_parent', 'temp_child')
println('Creating nested directory structure: ${nested_dir}...')
os.mkdir_all(nested_dir) or { println('Failed to create directory structure: ${err}') }
// os.mkdir creates a single new directory.
// os.is_dir checks if a path points to a directory.
// os.is_dir_empty checks if the directory has no files or subfolders.
// os.rmdir deletes a single empty directory.
single_dir := 'temp_single_dir'
os.mkdir(single_dir) or { println('Failed to create directory: ${err}') }
println('Is directory? ${os.is_dir(single_dir)}')
println('Is empty? ${os.is_dir_empty(single_dir)}')
os.rmdir(single_dir) or { println('Failed to remove directory: ${err}') }
// ==========================================
// 3. Path Manipulation & Extraction
// ==========================================
println('\n--- Path Manipulation & Extraction ---')
sample_path := '/usr/local/bin/v.exe'
// Path parsing helpers:
// os.dir returns the parent directory.
// os.base returns the last element of the path.
// os.file_ext returns the file suffix including dot.
// os.file_name returns the filename without the path.
// os.is_abs_path checks if the path starts with root.
// os.real_path resolves symlinks and relative references to return the absolute canonical path.
// os.norm_path cleans up and normalizes path separators.
// os.split_path splits a path into (dir, file_name, file_extension).
println('Sample path: ${sample_path}')
println('Directory: ${os.dir(sample_path)}')
println('Base name: ${os.base(sample_path)}')
println('Extension: ${os.file_ext(sample_path)}')
println('File name: ${os.file_name(sample_path)}')
println('Is absolute? ${os.is_abs_path(sample_path)}')
println('Real path: ${os.real_path('.')}')
println('Norm path: ${os.norm_path('/usr/local/../bin/v')}')
p_dir, p_file, p_ext := os.split_path(sample_path)
println('Split path -> dir: ${p_dir}, file: ${p_file}, ext: ${p_ext}')
// ==========================================
// 4. Working Directory Traversal
// ==========================================
println('\n--- Working Directory Traversal ---')
// os.getwd returns the current active working directory.
// os.chdir changes the current active working directory.
original_wd := os.getwd()
println('Original working directory: ${original_wd}')
println('Changing directory to: temp_parent...')
os.chdir('temp_parent') or { println('Failed to change directory: ${err}') }
println('New working directory: ${os.getwd()}')
// Restore original working directory
os.chdir(original_wd) or { println('Failed to restore directory: ${err}') }
// ==========================================
// 5. Advanced File Operations (Copying, Moving)
// ==========================================
println('\n--- Advanced File Actions ---')
copied_file := 'temp_book_copy.txt'
moved_file := 'temp_book_moved.txt'
// os.cp copies a file from source to destination.
println('Copying ${filename} to ${copied_file}...')
os.cp(filename, copied_file) or { println('Failed to copy file: ${err}') }
// os.mv moves or renames a file.
println('Moving ${copied_file} to ${moved_file}...')
os.mv(copied_file, moved_file) or { println('Failed to move file: ${err}') }
// ==========================================
// 6. Symbolic Links & Nix-Specific Operations
// ==========================================
println('\n--- Nix-Specific Operations ---')
symlink_name := 'temp_book_link.txt'
// os.symlink creates a symbolic link pointing to a target file.
// os.is_link checks if the path points to a symbolic link.
println('Creating symbolic link from ${moved_file} to ${symlink_name}...')
os.symlink(moved_file, symlink_name) or { println('Failed to create symlink: ${err}') }
if os.is_link(symlink_name) {
println('Confirmed: ${symlink_name} is a symbolic link.')
}
// os.chmod changes permission bits on a file (using octal representation).
// os.is_readable, os.is_writable, os.is_executable check specific accessibility bits.
println('Setting file permissions to 0o644 (read/write for owner, read-only for others)...')
os.chmod(moved_file, 0o644) or { println('Failed to change permissions: ${err}') }
println('Is readable? ${os.is_readable(moved_file)}')
println('Is writable? ${os.is_writable(moved_file)}')
println('Is executable? ${os.is_executable(moved_file)}')
// os.getuid and os.getgid get current user and group IDs.
// os.chown changes the user and group owner IDs on a file.
uid := os.getuid()
gid := os.getgid()
println('Setting ownership of ${moved_file} to UID: ${uid}, GID: ${gid}...')
os.chown(moved_file, uid, gid) or { println('Failed to change ownership: ${err}') }
// ==========================================
// 7. File Globbing (glob)
// ==========================================
println('\n--- File Globbing ---')
// os.glob finds all files matching a wildcard pattern (e.g. *.txt).
os.write_file('glob_test_1.txt', '1') or {}
os.write_file('glob_test_2.txt', '2') or {}
globbed_files := os.glob('glob_test_*.txt') or { [] }
println('Glob results: ${globbed_files}')
os.rm('glob_test_1.txt') or {}
os.rm('glob_test_2.txt') or {}
// ==========================================
// 8. Cleanup
// ==========================================
println('\n--- Cleanup ---')
// os.rm deletes a file.
// os.rmdir_all recursively removes a directory and all of its contents.
os.rm(filename) or { println('Failed to remove ${filename}: ${err}') }
os.rm(moved_file) or { println('Failed to remove ${moved_file}: ${err}') }
os.rm(symlink_name) or { println('Failed to remove symlink ${symlink_name}: ${err}') }
os.rmdir_all('temp_parent') or { println('Failed to remove temp_parent directory: ${err}') }
println('Cleanup completed successfully.')
}Os Process Pipe
In plain words: Shows the everyday os essentials: creating folders, writing text to a file (os.write_file), reading it back (os.read_file), listing files (os.ls), and deleting temporary files.
This example demonstrates managing subprocesses asynchronously using os.Process, exchanging data via stdin/stdout redirection, passing custom environments, sending POSIX signals (SIGSTOP, SIGCONT, SIGTERM), creating low-level descriptor pipes, and capturing stdout/stderr dynamically via IOCapture.
module main
import os
import time
fn main() {
println('=== V OS Processes, Pipes & Signals (POSIX/Nix) ===')
// --- 1. Spawning and Controlling Processes ---
println('\n--- 1. Asynchronous Child Process (Process) ---')
// Spawning '/bin/cat' as a child process
mut p := os.new_process('/bin/cat')
p.set_args([])
p.set_environment({
'CUSTOM_ENV_VAR': 'V-OS-Demo'
})
// Enable standard I/O redirection to interact with the process
p.set_redirect_stdio()
p.use_stdio_ctl = true
// Start the process asynchronously
p.run()
println('Child process spawned with PID: ${p.pid}')
println('Is alive? -> ${p.is_alive()}')
// Write to the process's standard input
p.stdin_write('Line 1: Hello from the parent process!\n')
p.stdin_write('Line 2: WebAssembly and V standard libraries rule.\n')
// Allow child process buffer to receive and echo the lines
time.sleep(100 * time.millisecond)
// Read output currently available in the stdout pipe
output := p.stdout_read()
println('Read from child stdout:\n${output.trim_space()}')
// --- 2. POSIX Signaling ---
println('\n--- 2. POSIX Signals ---')
// Suspend the child process (SIGSTOP)
println('Suspending child process (SIGSTOP)...')
p.signal_stop()
time.sleep(50 * time.millisecond)
// Resume the child process (SIGCONT)
println('Resuming child process (SIGCONT)...')
p.signal_continue()
time.sleep(50 * time.millisecond)
// Terminate the child process (SIGTERM)
println('Terminating child process (SIGTERM)...')
p.signal_term()
p.wait()
println('Child process exited with status: ${p.status} (Code: ${p.code})')
p.close()
// --- 3. Pipes ---
println('\n--- 3. Low-Level Descriptor Pipes (Pipe) ---')
// Create a new pipe
mut my_pipe := os.pipe() or {
println('Failed to create pipe: ${err}')
return
}
// Write to the pipe
pipe_msg := 'IPC via Pipe'.bytes()
written := my_pipe.write(pipe_msg) or {
println('Failed to write to pipe: ${err}')
0
}
println('Wrote ${written} bytes to pipe.')
// Read from the pipe
mut pipe_buf := []u8{len: 32}
bytes_read := my_pipe.read(mut pipe_buf) or {
println('Failed to read from pipe: ${err}')
0
}
println('Read message from pipe: "${pipe_buf[..bytes_read].bytestr()}"')
my_pipe.close()
// --- 4. Capture Stdout/Stderr ---
println('\n--- 4. Capture Stdout and Stderr (IOCapture) ---')
// Flush stdout to prevent capturing existing print statements
os.flush()
// Capture all stdout/stderr output within this block
mut cap := os.stdio_capture() or {
println('Failed to initialize capture: ${err}')
return
}
// Anything printed here will be redirected to the capture buffer
print('Captured standard output data.')
eprint('Captured standard error data.')
// Restore standard streams and retrieve captured data
captured_out, captured_err := cap.finish()
println('Captured stdout lines: ${captured_out}')
println('Captured stderr lines: ${captured_err}')
}Os System Info
In plain words: Shows the everyday os essentials: creating folders, writing text to a file (os.write_file), reading it back (os.read_file), listing files (os.ls), and deleting temporary files.
This example demonstrates calling system diagnostics (os.uname), retrieving current host/user identities, assessing disk capacity and usage metrics (os.disk_usage), and parsing detailed file metadata using POSIX stat/lstat mappings (os.Stat and os.FileMode).
module main
import os
fn main() {
println('=== V OS System & File Information (POSIX/Nix) ===')
// --- 1. System Info (uname, hostname, loginname) ---
println('\n--- 1. System Diagnostics ---')
// os.uname() returns kernel details, release, OS name, and architecture
u := os.uname()
println('Operating System: ${u.sysname}')
println('Node Name (Network): ${u.nodename}')
println('Kernel Release: ${u.release}')
println('Kernel Version: ${u.version}')
println('Machine Architecture: ${u.machine}')
// Retrieve hostname and login user name
host := os.hostname() or { 'unknown_host' }
user := os.loginname() or { 'unknown_user' }
println('Hostname: ${host}')
println('Login Name: ${user}')
// V standard user/system directories
println('User OS: ${os.user_os()}')
println('Home Directory: ${os.home_dir()}')
println('Temp Directory: ${os.temp_dir()}')
println('Config Directory: ${os.config_dir() or { 'N/A' }}')
println('Cache Directory: ${os.cache_dir()}')
println('Data Directory: ${os.data_dir()}')
// Executable details
println('Current Executable: ${os.executable()}')
println('Git Abs Path: ${os.find_abs_path_of_executable('git') or { 'not found' }}')
// Optional environment access & full environment map
println('Home via getenv_opt: ${os.getenv_opt('HOME') or { 'not set' }}')
env_map := os.environ()
// Safely print first few environment keys if available
limit := if env_map.len < 3 { env_map.len } else { 3 }
println('Sample Env Keys: ${env_map.keys()[..limit]}')
// --- 2. Identity and Process Metrics ---
println('\n--- 2. User/Group IDs & Process Context ---')
// Real and effective UID/GIDs
println('User ID (UID): ${os.getuid()}')
println('Group ID (GID): ${os.getgid()}')
println('Effective UID (EUID): ${os.geteuid()}')
println('Effective GID (EGID): ${os.getegid()}')
// Current Process ID and Parent Process ID
println('Process ID (PID): ${os.getpid()}')
println('Parent PID (PPID): ${os.getppid()}')
// --- 3. Disk Space Usage ---
println('\n--- 3. Disk Space Stats ---')
// Query disk space information for the current directory
du := os.disk_usage('.') or {
println('Failed to retrieve disk usage: ${err}')
return
}
// Convert u64 bytes to Gigabytes for user readability
total_gb := f64(du.total) / (1024.0 * 1024.0 * 1024.0)
avail_gb := f64(du.available) / (1024.0 * 1024.0 * 1024.0)
used_gb := f64(du.used) / (1024.0 * 1024.0 * 1024.0)
println('Disk Total: ${total_gb:.2f} GB')
println('Disk Available: ${avail_gb:.2f} GB')
println('Disk Used: ${used_gb:.2f} GB')
// --- 4. Detailed File Metadata (stat/lstat) ---
println('\n--- 4. File Metadata via stat ---')
// Let's create a temporary file to run stat on
temp_file := 'temp_stat_test.txt'
os.write_file(temp_file, 'V stat demo content.') or { return }
// Fetch file stats
st := os.stat(temp_file) or {
println('Failed to stat file: ${err}')
os.rm(temp_file) or {}
return
}
println('File Size: ${st.size} bytes')
println('Inode Number: ${st.inode}')
println('Hard Links Count: ${st.nlink}')
println('Device ID: ${st.dev}')
println('Owner UID: ${st.uid}')
println('Owner GID: ${st.gid}')
// Access access, modify, and status change timestamps
println('Last Access Time: ${st.atime} (Unix Epoch)')
println('Last Modify Time: ${st.mtime} (Unix Epoch)')
println('Last Change Time: ${st.ctime} (Unix Epoch)')
// File type and permissions from Stat
file_type := st.get_filetype()
file_mode := st.get_mode()
println('File Type: ${file_type}') // e.g., regular, directory, link, etc.
println('File Mode Bitmask: 0o${file_mode.bitmask():o}') // octal representation
// Permissions breakdown
println('Permissions -> Owner: R=${file_mode.owner.read} W=${file_mode.owner.write} X=${file_mode.owner.execute}')
println(' Group: R=${file_mode.group.read} W=${file_mode.group.write} X=${file_mode.group.execute}')
println(' Other: R=${file_mode.others.read} W=${file_mode.others.write} X=${file_mode.others.execute}')
// Cleanup
os.rm(temp_file) or {}
}Time And Stopwatch
In plain words: The time module handles dates, UTC/local timestamps, formatting dates as readable strings, sleeping (time.sleep), and measuring elapsed time.
V's standard library provides a robust and precise set of utilities for time retrieval, formatting, parsing, timezone management, and execution timing via the time module. Here is the simplest, most practical guide to when you should actually use each of these tools in real-world programming.
1. Time Retrieval & Fields
- The Vibe: "Checking your wristwatch."
- What it does: Gets the exact current date, time, and nanoseconds.
- Best to use when: You need to timestamp actions or log events.
- Real-world example: Recording when a user logs in.
2. Time Arithmetic & Comparisons
- The Vibe: "Time-traveling and deadlines."
- What it does: Adds or subtracts intervals (days, hours, seconds) and compares which time comes first.
- Best to use when: You need to calculate expirations or duration differences.
- Real-world example: Setting a user token to expire in 2 hours.
3. String Formatting & RFC Standards
- The Vibe: "The translator for calendar dates."
- What it does: Converts raw timestamps into clean, human-readable formats, custom strings, or RFC 3339 standards.
- Best to use when: Displaying dates to users or sending structured time JSON over APIs.
- Real-world example: Printing a post publish date as
YYYY-MM-DD HH:mm:ss.
4. Timezone Conversions (Local & UTC)
- The Vibe: "The jet-lag cure."
- What it does: Converts times between local system time and UTC.
- Best to use when: You store timestamps in UTC (best practice) but need to show them in the user's local timezone.
- Real-world example: Normalizing database entries to UTC time.
5. Relative Time
- The Vibe: "Social media date labels."
- What it does: Formats dates relative to now (e.g., "5 minutes ago", "yesterday").
- Best to use when: Displaying activity feeds or notification boards.
- Real-world example: Showing how long ago a comment was posted.
6. Stopwatch
- The Vibe: "The performance racing timer."
- What it does: Measures sub-millisecond elapsed durations.
- Best to use when: Benchmarking code speed or tracking long-running tasks.
- Real-world example: Measuring how long an API database query took to run.
module main
import time
fn main() {
println('Time API examples')
println('=================')
// time.now() returns the current system time.
// We can access properties like year, month, day, hour, etc.
now := time.now()
println('Current time: ${now}')
println('Fields -> year=${now.year}, month=${now.month}, day=${now.day}, hour=${now.hour}, minute=${now.minute}, second=${now.second}, nanosecond=${now.nanosecond}, is_local=${now.is_local}')
// ==========================================
// Arithmetic and comparisons
// ==========================================
// now.add() adds a duration to the timestamp.
// now.add_days() adds a specified number of days.
// now.add_seconds() adds a specified number of seconds.
// We can compare Time objects using <, >, ==, and subtract them to get a Duration.
future := now.add(2 * time.hour)
tomorrow := now.add_days(1)
in_30_seconds := now.add_seconds(30)
println('add: ${future}')
println('add_days: ${tomorrow}')
println('add_seconds: ${in_30_seconds}')
println('comparison: now < future -> ${now < future}')
println('comparison: now == now -> ${now == now}')
println('difference: future - now -> ${future - now}')
// ==========================================
// Formatting helpers
// ==========================================
// now.clean() formats time as YYYY-MM-DD HH:MM:SS.
// now.clean12() formats time using a 12-hour clock with AM/PM.
// now.custom_format() formats time using a custom layout pattern.
// now.format() and format_rfc3339() print standard ISO/RFC timestamps.
// format_ss methods print time down to micro, milli, or nanoseconds.
// strftime() uses C-like format specifiers.
println('clean: ${now.clean()}')
println('clean12: ${now.clean12()}')
println('custom_format: ${now.custom_format('YYYY-MM-DD HH:mm:ss')}')
println('format: ${now.format()}')
println('format_rfc3339: ${now.format_rfc3339()}')
println('format_rfc3339_micro: ${now.format_rfc3339_micro()}')
println('format_rfc3339_nano: ${now.format_rfc3339_nano()}')
println('format_ss: ${now.format_ss()}')
println('format_ss_micro: ${now.format_ss_micro()}')
println('format_ss_milli: ${now.format_ss_milli()}')
println('format_ss_nano: ${now.format_ss_nano()}')
println('strftime: ${now.strftime('%Y-%m-%d %H:%M:%S')}')
println('get_fmt_str: ${now.get_fmt_str(time.FormatDelimiter.hyphen, time.FormatTime.hhmm24,
time.FormatDate.yyyymmdd)}')
println('get_fmt_date_str: ${now.get_fmt_date_str(time.FormatDelimiter.hyphen, time.FormatDate.yyyymmdd)}')
println('get_fmt_time_str: ${now.get_fmt_time_str(time.FormatTime.hhmm24)}')
// ==========================================
// Date and time helpers
// ==========================================
// Extra details like day_of_week(), days_from_unix_epoch(), week_of_year(), smonth(), etc.
println('day_of_week: ${now.day_of_week()}')
println('days_from_unix_epoch: ${now.days_from_unix_epoch()}')
println('ddmmy: ${now.ddmmy()}')
println('hhmm: ${now.hhmm()}')
println('hhmm12: ${now.hhmm12()}')
println('hhmmss: ${now.hhmmss()}')
println('long_weekday_str: ${now.long_weekday_str()}')
println('md: ${now.md()}')
println('smonth: ${now.smonth()}')
println('weekday_str: ${now.weekday_str()}')
println('week_of_year: ${now.week_of_year()}')
println('year_day: ${now.year_day()}')
println('ymmdd: ${now.ymmdd()}')
// ==========================================
// UTC and local conversions
// ==========================================
// Convert between UTC and the system local timezone.
// unix(), unix_milli(), etc. return timestamps since the Unix Epoch.
println('is_utc: ${now.is_utc()}')
println('as_local: ${now.as_local()}')
println('as_utc: ${now.as_utc()}')
println('local: ${now.local()}')
println('local_to_utc: ${now.local_to_utc()}')
println('utc_to_local: ${now.utc_to_local()}')
println('local_unix: ${now.local_unix()}')
println('unix: ${now.unix()}')
println('unix_micro: ${now.unix_micro()}')
println('unix_milli: ${now.unix_milli()}')
println('unix_nano: ${now.unix_nano()}')
println('utc_string: ${now.utc_string()}')
// ==========================================
// Relative and serialization helpers
// ==========================================
// relative() and relative_short() return values like "2 hours ago".
// to_json() returns the JSON representation of the time.
// push_to_http_header() format HTTP-standard cookie/caching header dates.
println('relative: ${now.relative()}')
println('relative_short: ${now.relative_short()}')
println('debug: ${now.debug()}')
println('str: ${now.str()}')
println('to_json: ${now.to_json()}')
mut header_buffer := []u8{}
now.push_to_http_header(mut header_buffer)
println('http_header_string: ${now.http_header_string()}')
println('push_to_http_header: ${header_buffer.bytestr()}')
// ==========================================
// JSON parsing helpers
// ==========================================
// Parse Unix timestamps or ISO/RFC 3339 strings directly back into a Time struct.
mut parsed_from_number := time.now()
parsed_from_number.from_json_number('1712345678') or {
println('from_json_number error: ${err}')
}
println('from_json_number: ${parsed_from_number}')
mut parsed_from_string := time.now()
parsed_from_string.from_json_string('2024-04-06T12:34:56Z') or {
println('from_json_string error: ${err}')
}
println('from_json_string: ${parsed_from_string}')
// ==========================================
// Stopwatch example
// ==========================================
// new_stopwatch starts a new stopwatch to measure high-precision elapsed code execution time.
println('Starting stopwatch...')
mut sw := time.new_stopwatch()
time.sleep(150 * time.millisecond)
println('Elapsed: ${sw.elapsed().milliseconds()} ms')
}Http Client
In plain words: V's net.http module makes fetching data from websites or web APIs as easy as calling http.get('https://...') or http.post(...).
What happens in this example:
- Code note: 1. HTTP GET Request
- Code note: Reading a response header
- Code note: 2. HTTP POST Request
module main
import net.http
fn main() {
// 1. HTTP GET Request
println('Sending GET request to vlang.io...')
get_resp := http.get('https://vlang.io') or {
println('GET request failed: ${err}')
return
}
println('GET Status Code: ${get_resp.status_code}')
// Reading a response header
content_type := get_resp.header.get(.content_type) or { 'unknown' }
println('GET Content-Type Header: ${content_type}')
println('GET Body length: ${get_resp.body.len} bytes\n')
// 2. HTTP POST Request
println('Sending POST request to httpbin.org...')
post_body := 'Hello V Standard Library!'
post_resp := http.post('https://httpbin.org/post', post_body) or {
println('POST request failed: ${err}')
return
}
println('POST Status Code: ${post_resp.status_code}')
println('POST Response Body:')
println(post_resp.body)
}Regex Matching
In plain words: Regular expressions (regex) let you search text for patterns (like finding all email addresses, phone numbers, or dates inside a document).
What happens in this example:
- Code note: replacecallback is used by replaceby_fn() to show how a match can be rewritten.
- Code note: This sample text contains numbers and words that the regex API will inspect.
- Code note: Compile a regex that finds one or more digits.
- Code note: Create another regex object and compile a word-matching pattern.
module main
import regex
// replace_callback is used by replace_by_fn() to show how a match can be rewritten.
fn replace_callback(re regex.RE, in_txt string, start int, end int) string {
return '[${start}-${end}]'
}
fn main() {
// This sample text contains numbers and words that the regex API will inspect.
text := 'We have 15 apples, 32 bananas, and 120 oranges.'
// Compile a regex that finds one or more digits.
mut re := regex.regex_opt(r'\d+') or {
println('Failed to compile regex: ${err}')
return
}
// Create another regex object and compile a word-matching pattern.
mut re_from_new := regex.new()
re_from_new.compile_opt(r'\w+') or {
println('compile_opt() failed: ${err}')
return
}
// regex_base() returns the compiled regex plus a status code and error message.
base_re, base_code, base_err := regex.regex_base(r'\d+')
println('regex_base(): ${base_code}, ${base_err}')
println('regex_base query: ${base_re.get_query()}')
println('=== regex module demo ===')
println('query: ${re.get_query()}')
// find() returns the first match position and span.
start, end := re.find(text)
if start >= 0 {
matched := text[start..end]
println('find(): "${matched}" at (${start}, ${end})')
} else {
println('find(): no match')
}
// The next calls demonstrate the other common regex helpers.
println('find_from(): ${re.find_from(text, 10)}')
println('find_all(): ${re.find_all(text)}')
println('find_all_str(): ${re.find_all_str(text)}')
println('match_string(): ${re.match_string(text)}')
println('matches_string(): ${re.matches_string(text)}')
println('replace(): ${re.replace(text, 'NUM')}')
println('replace_n(): ${re.replace_n(text, 'NUM', 2)}')
println('replace_simple(): ${re.replace_simple(text, 'NUM')}')
println('replace_by_fn(): ${re.replace_by_fn(text, replace_callback)}')
println('split(): ${re.split(text)}')
println('get_group_list(): ${re.get_group_list()}')
println('get_code(): ${re.get_code()}')
println('get_group_by_id(): ${re.get_group_by_id(text, 0)}')
println('get_group_by_name(): ${re.get_group_by_name(text, '')}')
println('get_group_bounds_by_id(): ${re.get_group_bounds_by_id(0)}')
println('get_group_bounds_by_name(): ${re.get_group_bounds_by_name('')}')
println('match_base(): ${unsafe { re.match_base(text.str, text.len) }}')
// reset() clears the regex state so we can reuse the object.
re.reset()
println('reset() query: ${re.get_query()}')
println('new() compile_opt query: ${re_from_new.get_query()}')
}Command Line Flags
In plain words: The flag module parses command-line options (like --port 8080 or -v) passed to your terminal program and automatically generates a --help screen.
What happens in this example:
- Code note: 1. Initialize the flag parser with command line arguments (os.args)
- Code note: 2. Skip the executable name during parsing
- Code note: 3. Define flags with their types, short abbreviations, default values, and descriptions
- Code note: The second argument is a u8 rune for the short flag (e.g.
nfor -n), or0for none.
module main
import flag
import os
fn main() {
// 1. Initialize the flag parser with command line arguments (os.args)
mut fp := flag.new_flag_parser(os.args)
fp.application('greet-tool')
fp.version('1.0.0')
fp.description("A simple CLI greeting utility demonstrating V's flag module.")
// 2. Skip the executable name during parsing
fp.skip_executable()
// 3. Define flags with their types, short abbreviations, default values, and descriptions
// The second argument is a u8 rune for the short flag (e.g. `n` for -n), or `0` for none.
name := fp.string('name', `n`, 'Guest', 'The name of the person to greet')
verbose := fp.bool('verbose', `v`, false, 'Enable verbose logging output')
count := fp.int('count', `c`, 1, 'Number of times to print the greeting')
// 4. Finalize parsing. This returns remaining non-flag arguments or an error.
additional_args := fp.finalize() or {
println('Error: ${err}')
println(fp.usage())
return
}
if verbose {
println('Verbose Mode: ON')
println('Parsing completed successfully.')
}
// 5. Use the parsed variables
for i in 0 .. count {
println('Hello, ${name}! (greeting ${i + 1}/${count})')
}
if additional_args.len > 0 {
println('Additional non-flag arguments: ${additional_args}')
}
}Datatypes Collections
In plain words: Explores V's extra datatypes containers: LinkedList, DoublyLinkedList, Stack (last-in, first-out), Queue (first-in, first-out), Set (unique items only), MinHeap, and BST.
V's standard library provides a rich set of built-in collections and data structures through the datatypes module. Here is the simplest, most practical guide to when you should actually use each of these data structures in real-world programming.
1. Bloom Filter
- The Vibe: The "Fast bouncer at the door."
- What it does: It tells you with 100% certainty if something is not there, but if it says something is there, it might be guessing (a false positive).
- Best to use when: You have a massive database and searching it takes too long. You use a Bloom Filter as a quick shield. If the filter says "Nope, that username doesn't exist," you don't waste time searching the database.
- Real-world example: Checking if a chosen username is taken, or filtering out malicious URLs before loading a website.
2. Set
- The Vibe: The "No Duplicates Allowed" club.
- What it does: Stores a collection of items where everything must be unique. It also lets you do math operations like combining two groups (Union) or finding what they have in common (Intersection).
- Best to use when: You need to filter out duplicates instantly, or you need to compare two groups of data to find common ground.
- Real-world example: Storing unique visitor IP addresses on a website, or finding a list of mutual friends between you and someone else.
3. Queue
- The Vibe: Waiting in line at a grocery store (First In, First Out / FIFO).
- What it does: The first item you put in is the first item you take out.
- Best to use when: You have tasks that need to be processed exactly in the order they arrived.
- Real-world example: A printer queue handling documents, or customer support tickets waiting to be answered by an agent.
4. Stack
- The Vibe: A stack of dinner plates (Last In, First Out / LIFO).
- What it does: The last item you put on top is the first one you have to take off.
- Best to use when: You need to keep track of a history of actions so you can reverse them, or track active processes.
- Real-world example: The "Undo" ($Ctrl+Z$) feature in a text editor, or the "Back" button history in your web browser.
5. Ring Buffer (Circular Buffer)
- The Vibe: A streaming video that continuously overwrites itself.
- What it does: A queue with a strict maximum size. When it gets full, new data wraps around to the beginning and overwrites the oldest data.
- Best to use when: You are handling a continuous stream of data and you only care about the most recent information, without wasting memory.
- Real-world example: Audio/video streaming playback buffers, or a flight data recorder ("black box") that only saves the last 24 hours of flight data.
6. Min Heap
- The Vibe: A VIP line where the most urgent person always gets to go first.
- What it does: A specialized structure that always keeps the smallest (or highest priority) value at the very top.
- Best to use when: You need to constantly pull the lowest/highest value out of a changing list without sorting the entire list every single time.
- Real-world example: A hospital emergency room triage system, or GPS apps calculating the shortest route dynamically.
7. BSTree (Binary Search Tree)
- The Vibe: A perfectly organized filing cabinet.
- What it does: Keeps data sorted automatically as you add it. Smaller numbers go left, larger numbers go right.
- Best to use when: You need to search, add, and delete items constantly, and you always need the data to stay in perfect alphabetical or numerical order.
- Real-world example: File systems on your computer, or database indexing to make searching millions of records instant.
8. LinkedList vs. DoublyLinkedList
- The Vibe: A scavenger hunt. Item A gives you a clue to find Item B, which gives you a clue to find Item C.
- What it does:
- LinkedList (Singly): Each item points only to the next item. You can only move forward.
- DoublyLinkedList: Each item points to both the next item and the previous item. You can move forward and backward.
- Best to use when: You are constantly adding or removing items from the very beginning or middle of a list. (Standard arrays are slow at this because they have to shift all the other items over; Linked Lists just change where the "clues" point).
- Real-world example: A music playlist. A regular linked list only lets you hit "Next". A doubly linked list lets you hit "Next" and "Previous".
module main
import datatypes
// This helper creates a stable hash value for the bloom filter demo.
fn hash_string(value string) u32 {
mut hash := u32(2166136261)
for ch in value {
hash ^= u32(ch)
hash *= 16777619
}
return hash
}
fn main() {
println('=== datatypes collection demo ===')
// BloomFilter is a probabilistic structure used to test membership quickly.
println('\n--- BloomFilter ---')
mut bloom := datatypes.new_bloom_filter[string](hash_string, 64, 3) or { panic(err) }
bloom.add('apple')
bloom.add('banana')
bloom_exists_apple := bloom.exists('apple')
println('bloom exists apple: ${bloom_exists_apple}')
bloom_exists_cherry := bloom.exists('cherry')
println('bloom exists cherry: ${bloom_exists_cherry}')
mut bloom_fast := datatypes.new_bloom_filter[string](hash_string, 64, 3) or { panic(err) }
bloom_fast.add('date')
fast_exists_date := bloom_fast.exists('date')
println('fast bloom exists date: ${fast_exists_date}')
// The union and intersection methods combine two bloom filters.
union_bloom := bloom.@union(bloom_fast) or { panic(err) }
union_exists_banana := union_bloom.exists('banana')
println('union bloom exists banana: ${union_exists_banana}')
intersection_bloom := bloom.intersection(bloom_fast) or { panic(err) }
intersection_exists_apple := intersection_bloom.exists('apple')
println('intersection bloom exists apple: ${intersection_exists_apple}')
// BSTree stores values in sorted order and supports tree traversal.
println('\n--- BSTree ---')
mut bst := datatypes.BSTree[int]{}
bst_is_empty := bst.is_empty()
println('empty before inserts: ${bst_is_empty}')
bst.insert(10)
bst.insert(5)
bst.insert(15)
bst.insert(12)
bst_contains_12 := bst.contains(12)
println('contains 12: ${bst_contains_12}')
in_order := bst.in_order_traversal()
println('in_order: ${in_order}')
pre_order := bst.pre_order_traversal()
println('pre_order: ${pre_order}')
post_order := bst.post_order_traversal()
println('post_order: ${post_order}')
left_val := bst.to_left(10) or { -1 }
println('left of 10: ${left_val}')
right_val := bst.to_right(10) or { -1 }
println('right of 10: ${right_val}')
min_val := bst.min() or { -1 }
println('min: ${min_val}')
max_val := bst.max() or { -1 }
println('max: ${max_val}')
bst.remove(5)
bst_contains_5_after_remove := bst.contains(5)
println('contains 5 after remove: ${bst_contains_5_after_remove}')
// DoublyLinkedList supports inserting and iterating from both ends.
println('\n--- DoublyLinkedList ---')
mut dll := datatypes.DoublyLinkedList[string]{}
dll.push_back('one')
dll.push_front('zero')
dll.push_many(['two', 'three'], datatypes.Direction.back)
dll_array := dll.array()
println('dll array: ${dll_array}')
dll_first := dll.first() or { 'none' }
println('dll first: ${dll_first}')
dll_last := dll.last() or { 'none' }
println('dll last: ${dll_last}')
dll_index_two := dll.index('two') or { -1 }
println('dll index of two: ${dll_index_two}')
dll.insert(2, 'inserted') or { panic(err) }
dll_after_insert := dll.array()
println('dll after insert: ${dll_after_insert}')
dll.delete(1)
dll_after_delete := dll.array()
println('dll after delete: ${dll_after_delete}')
dll_str := dll.str()
println('dll str: ${dll_str}')
dll_next := dll.next() or { 'none' }
println('dll next: ${dll_next}')
mut dll_iter := dll.iterator()
for {
if value := dll_iter.next() {
println('dll iter: ${value}')
} else {
break
}
}
mut dll_back_iter := dll.back_iterator()
for {
if value := dll_back_iter.next() {
println('dll back iter: ${value}')
} else {
break
}
}
dll_pop_front := dll.pop_front() or { 'none' }
println('dll pop_front: ${dll_pop_front}')
dll_pop_back := dll.pop_back() or { 'none' }
println('dll pop_back: ${dll_pop_back}')
dll_final := dll.array()
println('dll final: ${dll_final}')
// LinkedList shows a simple singly linked sequence with push/pop helpers.
println('\n--- LinkedList ---')
mut linked_list := datatypes.LinkedList[int]{}
linked_list_is_empty := linked_list.is_empty()
println('linked list empty: ${linked_list_is_empty}')
linked_list.push(1)
linked_list.push(2)
linked_list.push_many([3, 4])
linked_list.prepend(0)
linked_list.insert(2, 5) or { panic(err) }
linked_list_array := linked_list.array()
println('linked list array: ${linked_list_array}')
linked_list_first := linked_list.first() or { -1 }
println('linked list first: ${linked_list_first}')
linked_list_last := linked_list.last() or { -1 }
println('linked list last: ${linked_list_last}')
linked_list_index_3 := linked_list.index(3) or { -1 }
println('linked list index 3: ${linked_list_index_3}')
linked_list_str := linked_list.str()
println('linked list str: ${linked_list_str}')
linked_list_pop := linked_list.pop() or { -1 }
println('linked list pop: ${linked_list_pop}')
linked_list_shift := linked_list.shift() or { -1 }
println('linked list shift: ${linked_list_shift}')
linked_list_next := linked_list.next() or { -1 }
println('linked list next: ${linked_list_next}')
mut list_iter := linked_list.iterator()
for {
if value := list_iter.next() {
println('linked list iter: ${value}')
} else {
break
}
}
linked_list_len := linked_list.len()
println('linked list len: ${linked_list_len}')
// MinHeap keeps the smallest value at the front.
println('\n--- MinHeap ---')
mut heap := datatypes.MinHeap[int]{}
heap.insert(8)
heap.insert(3)
heap.insert_many([5, 1, 7])
heap_len := heap.len()
println('heap len: ${heap_len}')
heap_peek := heap.peek() or { -1 }
println('heap peek: ${heap_peek}')
heap_pop_1 := heap.pop() or { -1 }
println('heap pop: ${heap_pop_1}')
heap_pop_2 := heap.pop() or { -1 }
println('heap pop: ${heap_pop_2}')
// Queue shows FIFO behavior and the standard enqueue/dequeue helpers.
println('\n--- Queue ---')
mut queue := datatypes.Queue[int]{}
queue_is_empty := queue.is_empty()
println('queue empty: ${queue_is_empty}')
queue.push(100)
queue.push(200)
queue.push(300)
queue_len := queue.len()
println('queue len: ${queue_len}')
queue_array := queue.array()
println('queue array: ${queue_array}')
queue_peek := queue.peek() or { -1 }
println('queue peek: ${queue_peek}')
queue_last := queue.last() or { -1 }
println('queue last: ${queue_last}')
queue_index_2 := queue.index(2) or { -1 }
println('queue index 2: ${queue_index_2}')
queue_str := queue.str()
println('queue str: ${queue_str}')
queue_pop_1 := queue.pop() or { -1 }
println('queue pop: ${queue_pop_1}')
queue_pop_2 := queue.pop() or { -1 }
println('queue pop: ${queue_pop_2}')
// RingBuffer provides bounded storage with wraparound behavior.
println('\n--- RingBuffer ---')
mut rb := datatypes.new_ringbuffer[string](4)
rb_is_empty := rb.is_empty()
println('rb empty: ${rb_is_empty}')
rb.push('first') or { panic(err) }
rb.push('second') or { panic(err) }
rb.push('third') or { panic(err) }
rb_occupied := rb.occupied()
println('rb occupied: ${rb_occupied}')
rb_remaining := rb.remaining()
println('rb remaining: ${rb_remaining}')
rb_pop := rb.pop() or { 'empty' }
println('rb pop: ${rb_pop}')
rb_pop_many := rb.pop_many(2) or { []string{} }
println('rb pop_many: ${rb_pop_many}')
rb_is_full := rb.is_full()
println('rb full: ${rb_is_full}')
rb.clear()
rb_after_clear := rb.is_empty()
println('rb after clear: ${rb_after_clear}')
// Set demonstrates unique values and set algebra operations.
println('\n--- Set ---')
mut set_a := datatypes.Set[string]{}
set_a.add_all(['apple', 'banana', 'cherry', 'apple'])
set_a_array := set_a.array()
println('set_a: ${set_a_array}')
set_a_size := set_a.size()
println('set_a size: ${set_a_size}')
set_a_contains_banana := set_a.exists('banana')
println('contains banana: ${set_a_contains_banana}')
set_a.remove('banana')
set_a_after_remove := set_a.array()
println('after remove: ${set_a_after_remove}')
set_a_pick := set_a.pick() or { 'empty' }
println('pick: ${set_a_pick}')
set_a_rest := set_a.rest() or { []string{} }
println('rest: ${set_a_rest}')
set_a_pop := set_a.pop() or { 'empty' }
println('pop: ${set_a_pop}')
set_a_is_empty := set_a.is_empty()
println('is_empty: ${set_a_is_empty}')
set_a.clear()
set_a_after_clear := set_a.is_empty()
println('cleared: ${set_a_after_clear}')
mut set_b := datatypes.Set[string]{}
set_b.add_all(['apple', 'cherry'])
mut set_c := datatypes.Set[string]{}
set_c.add_all(['cherry', 'date'])
union_set := set_b.@union(set_c).array()
println('union: ${union_set}')
intersection_set := set_b.intersection(set_c).array()
println('intersection: ${intersection_set}')
// Compute the difference manually to avoid V compiler/analyzer issues with generic operator overloading (-)
mut diff_set := set_b.copy()
for item in set_c.array() {
diff_set.remove(item)
}
diff_array := diff_set.array()
println('difference: ${diff_array}')
is_subset := set_b.subset(set_c)
println('subset: ${is_subset}')
copied_set := set_b.copy().array()
println('copy: ${copied_set}')
// Stack demonstrates LIFO behavior with push/pop operations.
println('\n--- Stack ---')
mut stack := datatypes.Stack[string]{}
stack_is_empty := stack.is_empty()
println('stack empty: ${stack_is_empty}')
stack.push('first')
stack.push('second')
stack.push('third')
stack_len := stack.len()
println('stack len: ${stack_len}')
stack_array := stack.array()
println('stack contents: ${stack_array}')
stack_peek := stack.peek() or { 'empty' }
println('stack peek: ${stack_peek}')
stack_str := stack.str()
println('stack str: ${stack_str}')
stack_pop_1 := stack.pop() or { 'empty' }
println('stack pop: ${stack_pop_1}')
stack_pop_2 := stack.pop() or { 'empty' }
println('stack pop: ${stack_pop_2}')
}Gg Graphics
In plain words: V's built-in gg module opens a hardware-accelerated desktop window and lets you draw 2D shapes, text, and interactive graphics on the screen in just a few lines of code! (For an even easier higher-level UI framework built on gg, check out simple_gg).
What happens in this example:
- Code note: AppContext holds the state of our graphical application.
- Code note: Using a state struct is a recommended best practice for gg applications
- Code note: to avoid global variables (which V does not support by default).
- Code note: Interactive shape parameters
module main
import gg
import math
// AppContext holds the state of our graphical application.
// Using a state struct is a recommended best practice for gg applications
// to avoid global variables (which V does not support by default).
struct AppContext {
mut:
ctx &gg.Context = unsafe { nil }
width int = 800
height int = 600
// Interactive shape parameters
shape_x f32 = 400.0
shape_y f32 = 300.0
shape_size f32 = 50.0
shape_color gg.Color = gg.blue
active_shape int // 0 = Circle, 1 = Rectangle, 2 = Triangle
// Tracking mouse positions and clicks
mouse_x f32
mouse_y f32
click_x f32 = -1.0
click_y f32 = -1.0
click_color gg.Color = gg.red
// Last key pressed message
last_key string = 'None'
}
fn main() {
// Initialize state
mut app := &AppContext{
width: 800
height: 600
}
// Create a new gg context.
// You specify callbacks for rendering frames, processing events,
// and cleanups, along with initial window configurations.
app.ctx = gg.new_context(
width: app.width
height: app.height
window_title: "V's gg graphics module: Tutorial & Interactive Demo"
bg_color: gg.rgb(240, 244, 248) // A subtle modern light-blue background
user_data: app // Pass our state struct to be accessible inside callbacks
frame_fn: frame // Callback called once per frame (to draw shapes/UI)
event_fn: on_event // Callback called for user inputs (mouse & keyboard)
)
println('Starting interactive graphics window...')
println('Controls:')
println(' - Move the mouse to see cursor position tracking.')
println(' - Left-Click anywhere to draw a red dot at the click location.')
println(' - Use Arrow Keys (Up/Down/Left/Right) to move the active shape.')
println(" - Press [C] or [c] to cycle the active shape's color.")
println(' - Press [S] or [s] to toggle between Circle, Rectangle, and Triangle.')
println(' - Press [Escape] to close the window.')
println('\nReal-world Case Study:')
println(' - Check out a complete journaling application built with gg:')
println(' https://github.com/codecaine-zz/MindSpace-Journal')
// Start the application main event loop.
app.ctx.run()
}
// frame is the drawing function called per frame.
// All rendering code MUST reside between ctx.begin() and ctx.end().
fn frame(data voidptr) {
mut app := unsafe { &AppContext(data) }
mut ctx := app.ctx
ctx.begin()
// --- 1. Draw Static 2D Shapes ---
// Draw a thick horizontal line dividing the header from the demo workspace
ctx.draw_line(0, 80, app.width, 80, gg.gray)
// Draw a filled rectangle (Left)
ctx.draw_rect_filled(50, 120, 120, 80, gg.green)
// Draw an empty/outline rectangle just next to it
ctx.draw_rect_empty(200, 120, 120, 80, gg.dark_gray)
// Draw a filled circle (Middle-Left)
ctx.draw_circle_filled(430, 160, 45, gg.orange)
// Draw an empty/outline circle
ctx.draw_circle_empty(560, 160, 45, gg.purple)
// Draw a filled triangle (Middle-Right)
ctx.draw_triangle_filled(700, 115, 750, 205, 650, 205, gg.pink)
// Draw a custom convex polygon (a star-like pentagon, bottom right)
poly_points := [
f32(650.0),
480.0, // Point 1
750.0,
480.0, // Point 2
780.0,
560.0, // Point 3
700.0,
520.0, // Point 4
620.0,
560.0, // Point 5
]
ctx.draw_convex_poly(poly_points, gg.cyan)
// --- 2. Render Text using custom configurations (TextCfg) ---
// Main Title with default configuration
ctx.draw_text_def(20, 15, "Welcome to V's Simple Graphics (gg) Tutorial!")
// Instructions and state metadata at the top right
ctx.draw_text(20, 45, 'Cursor: (${app.mouse_x:.1f}, ${app.mouse_y:.1f}) | Last Key: ${app.last_key}',
color: gg.dark_blue
size: 16
bold: true
)
// Context description
ctx.draw_text(50, 215, 'Filled & Empty Rectangles', size: 12, color: gg.dark_gray)
ctx.draw_text(400, 215, 'Filled & Empty Circles', size: 12, color: gg.dark_gray)
ctx.draw_text(650, 215, 'Filled Triangle', size: 12, color: gg.dark_gray)
ctx.draw_text(630, 570, 'Convex Polygon (Pentagon)', size: 12, color: gg.dark_gray)
// Help panel explaining key bindings
ctx.draw_rect_filled(20, 440, 280, 140, gg.Color{ r: 255, g: 255, b: 255, a: 180 })
ctx.draw_rect_empty(20, 440, 280, 140, gg.gray)
ctx.draw_text(35, 450, 'Controls Panel', size: 15, bold: true, color: gg.black)
ctx.draw_text(35, 475, '- Arrows: Move active shape', size: 13, color: gg.black)
ctx.draw_text(35, 495, '- C: Cycle shape color', size: 13, color: gg.black)
ctx.draw_text(35, 515, '- S: Switch shape type', size: 13, color: gg.black)
ctx.draw_text(35, 535, '- Mouse Click: Draw a dot', size: 13, color: gg.black)
ctx.draw_text(35, 555, '- Escape: Quit application', size: 13, color: gg.black)
// Case Study reference
ctx.draw_text(20, 585, 'Case Study: github.com/codecaine-zz/MindSpace-Journal',
size: 10
italic: true
color: gg.dark_blue
)
// --- 3. Draw Dynamic / Interactive Elements ---
// Draw a dot where the user clicked, if a click has occurred
if app.click_x >= 0.0 {
ctx.draw_circle_filled(app.click_x, app.click_y, 8, app.click_color)
ctx.draw_circle_empty(app.click_x, app.click_y, 12, gg.black)
ctx.draw_text(int(app.click_x) + 12, int(app.click_y) - 6, 'Last Click: (${app.click_x:.0f}, ${app.click_y:.0f})',
size: 11
color: gg.black
)
}
// Draw the active shape controlled by the user
match app.active_shape {
0 {
// Draw interactive Circle
ctx.draw_circle_filled(app.shape_x, app.shape_y, app.shape_size, app.shape_color)
ctx.draw_circle_empty(app.shape_x, app.shape_y, app.shape_size, gg.black)
}
1 {
// Draw interactive Rectangle (centered on coordinates)
half := app.shape_size
ctx.draw_rect_filled(app.shape_x - half, app.shape_y - half, app.shape_size * 2,
app.shape_size * 2, app.shape_color)
ctx.draw_rect_empty(app.shape_x - half, app.shape_y - half, app.shape_size * 2,
app.shape_size * 2, gg.black)
}
2 {
// Draw interactive Equilateral Triangle (centered on coordinates)
// Using trigonometry to draw an equilateral triangle of size `shape_size`
h := app.shape_size * f32(math.sqrt(3.0)) / 2.0
x1 := app.shape_x
y1 := app.shape_y - (2.0 / 3.0) * h
x2 := app.shape_x - app.shape_size
y2 := app.shape_y + (1.0 / 3.0) * h
x3 := app.shape_x + app.shape_size
y3 := app.shape_y + (1.0 / 3.0) * h
ctx.draw_triangle_filled(x1, y1, x2, y2, x3, y3, app.shape_color)
}
else {}
}
// Draw label above the active shape
ctx.draw_text(int(app.shape_x) - 40, int(app.shape_y) - int(app.shape_size) - 20,
'Active Shape',
size: 13
bold: true
color: gg.black
)
ctx.end()
}
// on_event intercepts and handles all system-level user inputs.
fn on_event(e &gg.Event, data voidptr) {
mut app := unsafe { &AppContext(data) }
mut ctx := app.ctx
match e.typ {
.mouse_move {
app.mouse_x = e.mouse_x
app.mouse_y = e.mouse_y
}
.mouse_down {
app.click_x = e.mouse_x
app.click_y = e.mouse_y
// Randomize click dot color slightly for visual variety
if app.click_color.r == 255 {
app.click_color = gg.Color{
r: 0
g: 180
b: 0
a: 255
}
} else {
app.click_color = gg.red
}
}
.key_down {
app.last_key = e.key_code.str()
match e.key_code {
.escape {
ctx.quit()
}
// Change Color when 'C' or 'c' is pressed
.c {
if app.shape_color.r == 0 && app.shape_color.b == 255 { // blue -> red
app.shape_color = gg.red
} else if app.shape_color.r == 255 && app.shape_color.g == 0 { // red -> green
app.shape_color = gg.green
} else { // green -> blue
app.shape_color = gg.blue
}
}
// Toggle shape type when 'S' or 's' is pressed
.s {
app.active_shape = (app.active_shape + 1) % 3
}
// Use Arrow Keys to move the active shape
.left {
app.shape_x -= 15.0
if app.shape_x < 0 {
app.shape_x = 0
}
}
.right {
app.shape_x += 15.0
if app.shape_x > app.width {
app.shape_x = f32(app.width)
}
}
.up {
app.shape_y -= 15.0
if app.shape_y < 80 {
app.shape_y = 80
}
// Keep below divider line
}
.down {
app.shape_y += 15.0
if app.shape_y > app.height {
app.shape_y = f32(app.height)
}
}
else {}
}
}
else {}
}
}Command Line Arguments
In plain words: Every word you type after your program's name in the terminal is stored in the os.args list (os.args[0] is the program path, and os.args[1..] are the user's arguments).
This example demonstrates how to directly access and parse command-line arguments using os.args to build simple command-line applications.
What happens in this example:
- Code note: os.args is a []string containing all command line arguments.
- Code note: os.args[0] is always the name of the executable (or the script path if run via v run).
- Code note: os.args[1..] contains the actual command-line arguments passed to the program.
module main
import os
fn main() {
// os.args is a []string containing all command line arguments.
// os.args[0] is always the name of the executable (or the script path if run via v run).
// os.args[1..] contains the actual command-line arguments passed to the program.
println('Executable / script path: ${os.args[0]}')
println('Total arguments count: ${os.args.len}')
println('All arguments list: ${os.args}')
if os.args.len < 2 {
println('\nUsage: v run command_line_arguments.v <command> [arguments...]')
println('Try running: v run command_line_arguments.v greet Alice')
println('Try running: v run command_line_arguments.v sum 3 5 8')
return
}
command := os.args[1]
args := os.args[2..]
println('\nProcessing command: "${command}" with args: ${args}')
match command {
'greet' {
if args.len < 1 {
println('Error: greet command requires a name.')
return
}
name := args[0]
println('Hello, ${name}!')
}
'sum' {
if args.len < 1 {
println('Error: sum command requires at least one number.')
return
}
mut total := 0
for arg in args {
num := arg.int()
total += num
}
println('Sum of numbers: ${total}')
}
else {
println('Unknown command: "${command}". Allowed commands: "greet", "sum".')
}
}
}Math And Rand
In plain words: Combines math (square roots, powers, trigonometry, rounding) with rand (generating random numbers, shuffling lists, and picking random items).
V's math and rand modules expose far more than the basic trigonometry and random-int helpers. The example below expands the repository walkthrough with additional documented functions from the current V docs, including exponentiation, clamping, logarithms, bytes, hex strings, ULIDs, and ranged integer generation.
module main
import math
import rand
fn main() {
println('=== Math & Rand Module Examples ===')
println('\n--- math ---')
println('Pi constant: ${math.pi}')
println('E constant: ${math.e}')
angle := 45.0 * (math.pi / 180.0)
println('sin(45 deg): ${math.sin(angle):.4f}')
println('cos(45 deg): ${math.cos(angle):.4f}')
println('tan(45 deg): ${math.tan(angle):.4f}')
println('2^10: ${math.pow(2.0, 10.0)}')
println('sqrt(144): ${math.sqrt(144.0)}')
println('ln(e): ${math.log(math.e)}')
println('log10(100): ${math.log10(100.0)}')
println('abs(-5.5): ${math.abs(-5.5)}')
println('max(10, 20): ${math.max(10.0, 20.0)}')
println('min(10, 20): ${math.min(10.0, 20.0)}')
println('ceil(4.2): ${math.ceil(4.2)}')
println('floor(4.8): ${math.floor(4.8)}')
println('round(4.5): ${math.round(4.5)}')
println('cbrt(27): ${math.cbrt(27.0):.2f}')
println('clamp(12, 0, 10): ${math.clamp(12.0, 0.0, 10.0)}')
println('exp(1): ${math.exp(1.0):.4f}')
println('exp2(3): ${math.exp2(3.0):.4f}')
println('hypot(3, 4): ${math.hypot(3.0, 4.0):.4f}')
println('log2(8): ${math.log2(8.0):.4f}')
println('trunc(4.9): ${math.trunc(4.9)}')
println('\n--- rand ---')
random_int := rand.int_in_range(1, 100) or { 0 }
println('Random integer in [1, 100): ${random_int}')
random_f64 := rand.f64()
println('Random f64 in [0.0, 1.0): ${random_f64:.4f}')
random_bool := (rand.intn(2) or { 0 }) == 0
println('Random boolean: ${random_bool}')
items := ['Apple', 'Banana', 'Cherry', 'Date']
chosen := rand.element(items) or { 'None' }
println('Randomly chosen fruit: ${chosen}')
random_bytes := rand.bytes(4) or { []u8{} }
println('Random bytes: ${random_bytes}')
random_hex := rand.hex(8)
println('Random hex string: ${random_hex}')
random_string := rand.string(8)
println('Random ascii string: ${random_string}')
random_ulid := rand.ulid()
println('Random ULID: ${random_ulid}')
random_i64 := rand.i64_in_range(i64(-10), i64(10)) or { 0 }
println('Random i64 in [-10, 10]: ${random_i64}')
mut uuid_str := rand.uuid_v4()
println('Random UUID v4: ${uuid_str}')
uuid_str = rand.uuid_v7()
println('Random UUID v7: ${uuid_str}')
}Crypto Asymmetric
In plain words: This security lesson (Crypto Asymmetric) demonstrates how V's built-in crypto and log modules handle hashing, encryption, digital signatures, and structured logging without external dependencies.
What happens in this example:
- Code note: --- 1. ECDSA ---
- Code note: Generate key pair
- Code note: Sign message
- Code note: Verify message
module main
import crypto.ecdsa
import crypto.ed25519
import crypto.pem
fn main() {
println('=== V Asymmetric Cryptography Demo ===')
message := 'Message to sign and verify asymmetric signatures.'.bytes()
// --- 1. ECDSA ---
println('\n--- ECDSA ---')
// Generate key pair
pub_ec, priv_ec := ecdsa.generate_key() or {
println('Failed to generate ECDSA key: ${err}')
return
}
// Sign message
sig_ec := priv_ec.sign(message, ecdsa.SignerOpts{}) or {
println('ECDSA signing failed: ${err}')
return
}
println('ECDSA Signature (Hex): ${sig_ec.hex()}')
// Verify message
verified_ec := pub_ec.verify(message, sig_ec, ecdsa.SignerOpts{}) or {
println('ECDSA verification error: ${err}')
return
}
println('ECDSA Signature Verified? -> ${verified_ec}')
// --- 2. Ed25519 ---
println('\n--- Ed25519 ---')
// Generate key pair
pub_ed, priv_ed := ed25519.generate_key() or {
println('Failed to generate Ed25519 key: ${err}')
return
}
// Sign message
sig_ed := ed25519.sign(priv_ed, message) or {
println('Ed25519 signing failed: ${err}')
return
}
println('Ed25519 Signature (Hex): ${sig_ed.hex()}')
// Verify message
verified_ed := ed25519.verify(pub_ed, message, sig_ed) or {
println('Ed25519 verification error: ${err}')
return
}
println('Ed25519 Signature Verified? -> ${verified_ed}')
// --- 3. PEM Encoding/Decoding ---
println('\n--- PEM (Privacy Enhanced Mail) Encoding ---')
pub_bytes := pub_ec.bytes() or {
println('Failed to get public key bytes: ${err}')
return
}
mut pem_block := pem.Block.new('EC PUBLIC KEY')
pem_block.data = pub_bytes
pem_string := pem_block.encode(pem.EncodeConfig{}) or {
println('PEM encoding failed: ${err}')
return
}
println('Encoded PEM Public Key:')
println(pem_string)
// Decode back
decoded_block, _ := pem.decode(pem_string) or {
println('PEM decoding failed')
return
}
println('Decoded Block Type: "${decoded_block.block_type}"')
println('Decoded data size matches? -> ${decoded_block.data.len == pub_bytes.len}')
}Crypto Entropy
In plain words: This security lesson (Crypto Entropy) demonstrates how V's built-in crypto and log modules handle hashing, encryption, digital signatures, and structured logging without external dependencies.
What happens in this example:
- Code note: --- 1. Generating Secure Random Bytes ---
- Code note: Generates securely generated random bytes from the OS entropy pool
- Code note: --- 2. Generating Secure Random u64 ---
- Code note: Generates a random u64 in the range [0, max)
module main
import crypto.rand
import math.big
fn main() {
println('=== V Secure Randomness (Entropy) Demo ===')
// --- 1. Generating Secure Random Bytes ---
println('\n--- Secure Random Bytes ---')
// Generates securely generated random bytes from the OS entropy pool
random_bytes := rand.bytes(16) or {
println('Failed to generate secure bytes: ${err}')
return
}
println('Generated 16 secure bytes (Hex): ${random_bytes.hex()}')
// --- 2. Generating Secure Random u64 ---
println('\n--- Secure Random u64 ---')
// Generates a random u64 in the range [0, max)
limit_u64 := u64(10_000)
random_val := rand.int_u64(limit_u64) or {
println('Failed to generate random u64: ${err}')
return
}
println('Secure random u64 in [0, ${limit_u64}): ${random_val}')
// --- 3. Generating Secure Random Big Integer ---
println('\n--- Secure Random big.Integer ---')
// Generates a random big.Integer in the range [0, limit)
limit_str := '10000000000000000000000000000000000000000' // 10^40
limit_big := big.integer_from_string(limit_str) or {
println('Failed to parse big integer string: ${err}')
return
}
random_big := rand.int_big(limit_big) or {
println('Failed to generate random big integer: ${err}')
return
}
println('Secure random big.Integer in [0, 10^40):')
println(random_big.str())
}Crypto Hash
In plain words: Computes one-way digital fingerprints (hashes like MD5, SHA-1, SHA-256, SHA-512) to verify that a file or password hasn't been tampered with.
For a detailed demonstration of every cryptographic module, the standard library examples are structured into subdirectories under language_updates_and_stdlib/02_standard_library/11_log_and_crypto/crypto/.
1. Cryptographic Hash Functions
Demonstrates MD5, SHA-1, SHA-256, SHA-512, SHA-3 (Keccak-256/Keccak-512), RIPEMD-160, BLAKE2b, BLAKE2s, and BLAKE3.
2. Symmetric Ciphers & Block Modes
Demonstrates AES (CBC block mode with PKCS7-like padding), DES, Blowfish (encryption-only), RC4 stream cipher, and general block modes.
3. Asymmetric Cryptography & PEM Formats
Demonstrates ECDSA key generation, signing, and verification; Ed25519 signing and verification; and PEM block encoding/decoding.
4. Key Derivation Functions (KDF)
Demonstrates secure password hashing and key derivation using Bcrypt, Scrypt, and PBKDF2.
5. Message Authentication Codes (MAC)
Demonstrates message integrity and authenticity verification using HMAC-SHA256.
6. Secure Randomness & Entropy
Demonstrates generating secure cryptographically random bytes, u64 values, and large integers (big.Integer).
module main
import crypto.md5
import crypto.sha1
import crypto.sha256
import crypto.sha512
import crypto.sha3
import crypto.ripemd160
import crypto.blake2b
import crypto.blake2s
import crypto.blake3
fn main() {
println('=== V Cryptographic Hash Algorithms ===')
input := 'V Language Crypto Guide'.bytes()
input_str := 'V Language Crypto Guide'
// 1. MD5 (128-bit)
md5_hex := md5.hexhash(input_str)
println('MD5: ${md5_hex}')
// 2. SHA-1 (160-bit)
sha1_hex := sha1.hexhash(input_str)
println('SHA-1: ${sha1_hex}')
// 3. SHA-256 (256-bit)
sha256_hex := sha256.hexhash(input_str)
println('SHA-256: ${sha256_hex}')
// 4. SHA-512 (512-bit)
sha512_hex := sha512.hexhash(input_str)
println('SHA-512: ${sha512_hex}')
// 5. SHA-3 (Keccak-based, 256 and 512 bit sums)
sha3_256 := sha3.sum256(input)
sha3_512 := sha3.sum512(input)
println('SHA3-256: ${sha3_256.hex()}')
println('SHA3-512: ${sha3_512.hex()}')
// 6. RIPEMD-160 (160-bit)
ripemd_hex := ripemd160.hexhash(input_str)
println('RIPEMD160: ${ripemd_hex}')
// 7. BLAKE2b (commonly 512-bit / 256-bit)
blake2b_256 := blake2b.sum256(input)
blake2b_512 := blake2b.sum512(input)
println('BLAKE2b-256: ${blake2b_256.hex()}')
println('BLAKE2b-512: ${blake2b_512.hex()}')
// 8. BLAKE2s (commonly 256-bit)
blake2s_256 := blake2s.sum256(input)
println('BLAKE2s-256: ${blake2s_256.hex()}')
// 9. BLAKE3 (256-bit, highly optimized)
blake3_256 := blake3.sum256(input)
println('BLAKE3-256: ${blake3_256.hex()}')
}Crypto Kdf
In plain words: This security lesson (Crypto Kdf) demonstrates how V's built-in crypto and log modules handle hashing, encryption, digital signatures, and structured logging without external dependencies.
What happens in this example:
- Code note: --- 1. Bcrypt ---
- Code note: --- 2. Scrypt ---
- Code note: N=16384, r=8, p=1, key_len=32 (N must be power of 2)
- Code note: --- 3. PBKDF2 ---
module main
import crypto.bcrypt
import crypto.scrypt
import crypto.pbkdf2
import crypto.sha256
fn main() {
println('=== V Key Derivation Functions Demo ===')
// --- 1. Bcrypt ---
println('\n--- Bcrypt ---')
password := 'super_secure_password'.bytes()
hash := bcrypt.generate_from_password(password, bcrypt.default_cost) or {
println('Bcrypt failed: ${err}')
return
}
println('Bcrypt hash: ${hash}')
bcrypt.compare_hash_and_password(password, hash.bytes()) or {
println('Bcrypt verification failed: ${err}')
return
}
println('Bcrypt verification successful!')
// --- 2. Scrypt ---
println('\n--- Scrypt ---')
scrypt_pass := 'my_scrypt_pass'.bytes()
scrypt_salt := 'scrypt_salt'.bytes()
// N=16384, r=8, p=1, key_len=32 (N must be power of 2)
scrypt_key := scrypt.scrypt(scrypt_pass, scrypt_salt, 16384, 8, 1, 32) or {
println('Scrypt failed: ${err}')
return
}
println('Scrypt Key (Hex): ${scrypt_key.hex()}')
// --- 3. PBKDF2 ---
println('\n--- PBKDF2 ---')
pbkdf2_pass := 'my_pbkdf2_pass'.bytes()
pbkdf2_salt := 'pbkdf2_salt'.bytes()
// pbkdf2.key(password, salt, iterations, key_len, hash_fn)
pbkdf2_key := pbkdf2.key(pbkdf2_pass, pbkdf2_salt, 4096, 32, sha256.new()) or {
println('PBKDF2 failed: ${err}')
return
}
println('PBKDF2 Key (Hex): ${pbkdf2_key.hex()}')
}Crypto Mac
In plain words: This security lesson (Crypto Mac) demonstrates how V's built-in crypto and log modules handle hashing, encryption, digital signatures, and structured logging without external dependencies.
What happens in this example:
- Code note: --- 1. HMAC-SHA256 Signature Generation ---
- Code note: hmac.new(key, data, hash_func, blocksize)
- Code note: --- 2. HMAC Verification ---
- Code note: Re-compute to verify
module main
import crypto.hmac
import crypto.sha256
fn main() {
println('=== V Message Authentication Codes (MAC) Demo ===')
// --- 1. HMAC-SHA256 Signature Generation ---
println('\n--- HMAC-SHA256 Signature ---')
key := 'secret_signing_key'.bytes()
message := 'This is a message to be authenticated using HMAC.'.bytes()
// hmac.new(key, data, hash_func, blocksize)
mac := hmac.new(key, message, sha256.sum, sha256.block_size)
println('HMAC (Hex): ${mac.hex()}')
// --- 2. HMAC Verification ---
println('\n--- HMAC Verification ---')
// Re-compute to verify
computed_mac := hmac.new(key, message, sha256.sum, sha256.block_size)
// hmac.equal performs constant-time comparison to prevent timing attacks
is_valid := hmac.equal(mac, computed_mac)
println('Signature matches? -> ${is_valid}')
// Verify with a tampered message
tampered_message := 'This is a message to be authenticated using HMAC!'.bytes()
tampered_mac := hmac.new(key, tampered_message, sha256.sum, sha256.block_size)
is_tampered_valid := hmac.equal(mac, tampered_mac)
println('Tampered signature matches? -> ${is_tampered_valid}')
}Crypto Symmetric
In plain words: This security lesson (Crypto Symmetric) demonstrates how V's built-in crypto and log modules handle hashing, encryption, digital signatures, and structured logging without external dependencies.
What happens in this example:
- Code note: --- 1. AES with CBC Block Mode ---
- Code note: In CBC mode, data must be padded to block size (16 bytes for AES)
module main
import crypto.aes
import crypto.des
import crypto.blowfish
import crypto.rc4
import crypto.cipher
fn main() {
println('=== V Symmetric Cryptography Demo ===')
// --- 1. AES with CBC Block Mode ---
println('\n--- AES (CBC Mode) ---')
aes_key := [u8(1), 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] // 16-byte key (AES-128)
aes_iv := [u8(9), 8, 7, 6, 5, 4, 3, 2, 1, 0, 9, 8, 7, 6, 5, 4] // 16-byte IV
aes_block := aes.new_cipher(aes_key)
mut aes_enc := cipher.new_cbc(aes_block, aes_iv)
// In CBC mode, data must be padded to block size (16 bytes for AES)
plaintext := 'Hello, V Cryptography! Padding here.'.bytes() // 36 bytes. We need to pad it to 48 bytes (multiple of 16)
mut padded := plaintext.clone()
pad_len := 16 - (padded.len % 16)
for _ in 0 .. pad_len {
padded << u8(pad_len)
}
mut ciphertext := []u8{len: padded.len}
aes_enc.encrypt_blocks(mut ciphertext, padded)
println('Ciphertext (Hex): ${ciphertext.hex()}')
// Decrypt
mut aes_dec := cipher.new_cbc(aes_block, aes_iv)
mut decrypted := []u8{len: ciphertext.len}
aes_dec.decrypt_blocks(mut decrypted, ciphertext)
// Unpad
unpadded_len := decrypted.len - int(decrypted.last())
unpadded_text := decrypted[..unpadded_len].bytestr()
println('Decrypted Text: "${unpadded_text}"')
// --- 2. DES Block Cipher ---
println('\n--- DES ---')
des_key := [u8(1), 2, 3, 4, 5, 6, 7, 8] // 8-byte key
des_block := des.new_cipher(des_key)
des_plain := 'DESplain'.bytes() // exactly 8 bytes (DES block size)
mut des_cipher := []u8{len: 8}
des_block.encrypt(mut des_cipher, des_plain)
println('DES Ciphertext (Hex): ${des_cipher.hex()}')
mut des_decrypted := []u8{len: 8}
des_block.decrypt(mut des_decrypted, des_cipher)
println('DES Decrypted: "${des_decrypted.bytestr()}"')
// --- 3. Blowfish Block Cipher ---
println('\n--- Blowfish (Encryption Only) ---')
bf_key := 'blowfish_key'.bytes()
mut bf := blowfish.new_cipher(bf_key) or { panic(err) }
bf_plain := 'bf_block'.bytes() // exactly 8 bytes (Blowfish block size)
mut bf_cipher := []u8{len: 8}
bf.encrypt(mut bf_cipher, bf_plain)
println('Blowfish Ciphertext (Hex): ${bf_cipher.hex()}')
println('(Note: V standard library crypto.blowfish only supports encryption)')
// --- 4. RC4 Stream Cipher ---
println('\n--- RC4 (Stream Cipher) ---')
rc4_key := 'rc4_secret_key'.bytes()
rc4_plain := 'RC4 is a stream cipher commonly used for legacy operations.'.bytes()
mut rc4_enc := rc4.new_cipher(rc4_key) or { panic(err) }
mut rc4_cipher := []u8{len: rc4_plain.len}
rc4_enc.xor_key_stream(mut rc4_cipher, rc4_plain)
println('RC4 Ciphertext (Hex): ${rc4_cipher.hex()}')
mut rc4_dec := rc4.new_cipher(rc4_key) or { panic(err) }
mut rc4_decrypted := []u8{len: rc4_cipher.len}
rc4_dec.xor_key_stream(mut rc4_decrypted, rc4_cipher)
println('RC4 Decrypted: "${rc4_decrypted.bytestr()}"')
}Log And Crypto
In plain words: This security lesson (Log And Crypto) demonstrates how V's built-in crypto and log modules handle hashing, encryption, digital signatures, and structured logging without external dependencies.
What happens in this example:
- Code note: --- log ---
- Code note: V's log module provides customizable levels (debug, info, warn, error, fatal)
- Code note: --- crypto ---
- Code note: SHA256 Hash
module main
import log
import crypto.sha256
import crypto.md5
fn main() {
println('=== Log & Crypto Module Examples ===')
// --- log ---
println('\n--- log ---')
// V's log module provides customizable levels (debug, info, warn, error, fatal)
mut logger := log.Log{}
logger.set_level(.info) // Set threshold (ignores debug level)
logger.info('Logger initialized.')
logger.warn('This is a warning message.')
logger.error('This is an error message.')
// --- crypto ---
println('\n--- crypto ---')
input := 'V language standard library'
// SHA256 Hash
sha_hash := sha256.hexhash(input)
println('SHA-256 of "${input}":')
println(' ${sha_hash}')
// MD5 Hash
md5_hash := md5.hexhash(input)
println('MD5 of "${input}":')
println(' ${md5_hash}')
}Sync Concurrency
In plain words: The sync module provides low-level thread coordination tools like WaitGroup (wait for a group of background tasks to finish) and Mutex (a lock so only one thread accesses a resource at a time).
What happens in this example:
- Code note: 1. WaitGroup (Wait for multiple goroutines/tasks)
- Code note: 2. Mutex (Thread-safe shared state access)
module main
import sync
import time
fn worker(id int, mut wg sync.WaitGroup) {
defer {
wg.done()
}
println('Worker ${id} starting...')
time.sleep(50 * time.millisecond)
println('Worker ${id} done!')
}
fn main() {
println('=== Sync & Concurrency Examples ===')
// 1. WaitGroup (Wait for multiple goroutines/tasks)
mut wg := sync.new_waitgroup()
for i in 1 .. 4 {
wg.add(1)
go worker(i, mut wg)
}
wg.wait()
println('All workers completed!')
// 2. Mutex (Thread-safe shared state access)
println('\n=== Mutex Demo ===')
mut mu := sync.new_mutex()
mu.@lock()
println('Mutex locked')
mu.unlock()
println('Mutex unlocked')
}Encoding Formats
In plain words: V includes built-in modules for common data formats like CSV (spreadsheet tables), Base64 (turning binary bytes into safe text), Hex, and XML.
What happens in this example:
- Code note: --- 1. Base64 ---
- Code note: --- 2. Hex ---
- Code note: --- 3. CSV ---
module main
import encoding.base64
import encoding.hex
import encoding.csv
fn main() {
println('=== Encoding Modules Examples ===')
// --- 1. Base64 ---
println('\n--- Base64 ---')
raw_str := 'V Programming Language'
encoded_b64 := base64.encode_str(raw_str)
println('Encoded Base64: ${encoded_b64}')
decoded_b64 := base64.decode_str(encoded_b64)
println('Decoded Base64: ${decoded_b64}')
// --- 2. Hex ---
println('\n--- Hex ---')
raw_bytes := [u8(72), 101, 108, 108, 111] // "Hello"
encoded_hex := hex.encode(raw_bytes)
println('Encoded Hex: ${encoded_hex}')
decoded_hex := hex.decode(encoded_hex) or { []u8{} }
println('Decoded Hex: ${decoded_hex.bytestr()}')
// --- 3. CSV ---
println('\n--- CSV ---')
csv_data := 'Name,Age,City\nAlice,30,New York\nBob,25,San Francisco'
mut reader := csv.new_reader(csv_data)
println('Reading CSV rows:')
for {
row := reader.read() or { break }
println(' Row: ${row}')
}
}Arrays Utility
In plain words: The arrays module (import arrays) adds one-line helpers to find the min, max, sum, chunk (split into groups), flatten, group_by, and binary_search on any list!
V's arrays module provides helpers for searching, grouping, partitioning, folding, and reducing collections. The verified demo in this repository exercises many documented helpers, including extremum lookup, chunking, searching, uniqueness, partitioning, windowing, and rotations.
module main
import arrays
fn main() {
println('=== arrays Utility Module Examples ===')
nums := [5, 3, 9, 1, 7, 3]
sorted_nums := [1, 3, 5, 7, 9]
words := ['apple', 'banana', 'pear', 'banana']
repeated := [1, 1, 2, 2, 3, 3]
letters := ['a', 'b', 'c']
min_val := arrays.min(nums) or { 0 }
max_val := arrays.max(nums) or { 0 }
println('Array: ${nums}')
println('Min: ${min_val}')
println('Max: ${max_val}')
min_idx := arrays.idx_min(nums) or { -1 }
max_idx := arrays.idx_max(nums) or { -1 }
println('Index of Min: ${min_idx}')
println('Index of Max: ${max_idx}')
chunked := arrays.chunk(nums, 2)
println('chunk(): ${chunked}')
append_result := arrays.append(nums, [10, 11])
println('append(): ${append_result}')
concat_result := arrays.concat(nums, 10, 11)
println('concat(): ${concat_result}')
mut copy_result := []int{}
copied_count := arrays.copy(mut copy_result, nums)
println('copy(): ${copied_count} -> ${copy_result}')
distinct_result := arrays.distinct(words)
println('distinct(): ${distinct_result}')
arrays.each(nums, fn (elem int) {
println('each(): ${elem}')
})
arrays.each_indexed(nums, fn (i int, elem int) {
println('each_indexed(): ${i} -> ${elem}')
})
filtered := arrays.filter_indexed(nums, fn (idx int, elem int) bool {
return idx % 2 == 0 && elem > 3
})
println('filter_indexed(): ${filtered}')
first_match := arrays.find_first(nums, fn (elem int) bool {
return elem > 5
}) or { 0 }
println('find_first(): ${first_match}')
last_match := arrays.find_last(nums, fn (elem int) bool {
return elem > 5
}) or { 0 }
println('find_last(): ${last_match}')
flat_mapped := arrays.flat_map[int, string](nums, fn (elem int) []string {
return [elem.str(), '!']
})
println('flat_map(): ${flat_mapped}')
flat_mapped_indexed := arrays.flat_map_indexed[int, string](nums, fn (idx int, elem int) []string {
return ['${idx}', elem.str()]
})
println('flat_map_indexed(): ${flat_mapped_indexed}')
flattened := arrays.flatten([[1, 2], [3, 4], [5]])
println('flatten(): ${flattened}')
folded := arrays.fold(nums, 0, fn (acc int, elem int) int {
return acc + elem
})
println('fold(): ${folded}')
folded_indexed := arrays.fold_indexed(nums, 0, fn (idx int, acc int, elem int) int {
return acc + idx + elem
})
println('fold_indexed(): ${folded_indexed}')
println('group(): skipped in this sample because the helper expects variadic slices and is sensitive to local analyzer parsing')
grouped_by_parity := arrays.group_by(nums, fn (val int) string {
if val % 2 == 0 {
return 'even'
}
return 'odd'
})
println('group_by(): ${grouped_by_parity}')
binary_search_result := arrays.binary_search(sorted_nums, 7) or { -1 }
println('binary_search(): ${binary_search_result}')
c_array_example := unsafe { arrays.carray_to_varray[int](nil, 0) }
println('carray_to_varray(): ${c_array_example}')
chunked_while := arrays.chunk_while(nums, fn (before int, after int) bool {
return before < after
})
println('chunk_while(): ${chunked_while}')
index_first := arrays.index_of_first(nums, fn (idx int, elem int) bool {
return idx > 0 && elem == 3
})
println('index_of_first(): ${index_first}')
index_last := arrays.index_of_last(nums, fn (idx int, elem int) bool {
return idx > 0 && elem == 3
})
println('index_of_last(): ${index_last}')
joined := arrays.join_to_string(words, ' | ', fn (elem string) string {
return elem.to_upper()
})
println('join_to_string(): ${joined}')
lower := arrays.lower_bound(sorted_nums, 4) or { 0 }
upper := arrays.upper_bound(sorted_nums, 4) or { 0 }
println('lower_bound(): ${lower}')
println('upper_bound(): ${upper}')
mapped := arrays.map_indexed(nums, fn (idx int, elem int) int {
return idx + elem
})
println('map_indexed(): ${mapped}')
counts := arrays.map_of_counts([1, 2, 2, 3, 1])
println('map_of_counts(): ${counts}')
indexes := arrays.map_of_indexes([9, 1, 9, 4])
println('map_of_indexes(): ${indexes}')
merge_result := arrays.merge(letters, ['d'])
println('merge(): ${merge_result}')
partition_even, partition_odd := arrays.partition(nums, fn (elem int) bool {
return elem % 2 == 0
})
println('partition(): even=${partition_even}, odd=${partition_odd}')
reduce_result := arrays.reduce(nums, fn (acc int, elem int) int {
return acc + elem
}) or { 0 }
println('reduce(): ${reduce_result}')
reduce_indexed_result := arrays.reduce_indexed(nums, fn (idx int, acc int, elem int) int {
return acc + idx + elem
}) or { 0 }
println('reduce_indexed(): ${reduce_indexed_result}')
mut reverse_iter := arrays.reverse_iterator(nums)
println('reverse_iterator():')
for {
if value := reverse_iter.next() {
println(' ${*value}')
} else {
break
}
}
reverse_iter.free()
mut rotate_left_example := [1, 2, 3, 4, 5]
arrays.rotate_left(mut rotate_left_example, 2)
println('rotate_left(): ${rotate_left_example}')
mut rotate_right_example := [1, 2, 3, 4, 5]
arrays.rotate_right(mut rotate_right_example, 2)
println('rotate_right(): ${rotate_right_example}')
sum_result := arrays.sum(nums) or { 0 }
println('sum(): ${sum_result}')
unique_result := arrays.uniq(repeated)
println('uniq(): ${unique_result}')
uniq_all_result := arrays.uniq_all_repeated(repeated)
println('uniq_all_repeated(): ${uniq_all_result}')
uniq_only_result := arrays.uniq_only(repeated)
println('uniq_only(): ${uniq_only_result}')
uniq_only_repeated_result := arrays.uniq_only_repeated(repeated)
println('uniq_only_repeated(): ${uniq_only_repeated_result}')
window_result := arrays.window(nums, arrays.WindowAttribute{ size: 2, step: 1 })
println('window(): ${window_result}')
println('All arrays examples completed.')
}This section is intentionally broader than a quick smoke test; it mirrors the runnable repository example and highlights the helpers exposed by the module.
Toml
In plain words: Parses human-friendly .toml configuration files (like Cargo.toml or config.toml) and reads values like doc.value('server.port').int().
This example demonstrates how to parse and query TOML configuration files using V's built-in toml module.
What happens in this example:
- Code note: 1. Reading basic values using value() and type converters
- Code note: 2. Accessing nested tables
- Code note: 3. Accessing primitive values
- Code note: 4. Retrieving array values
module main
import toml
const toml_content = '
# TOML configuration example
title = "V TOML Demo"
[owner]
name = "Antigravity AI"
organization = "Google DeepMind"
[database]
server = "127.0.0.1"
ports = [ 5432, 5433 ]
connection_max = 1000
enabled = true
'
fn main() {
println('=== TOML Module Demo ===')
doc := toml.parse_text(toml_content) or {
println('Failed to parse TOML: ${err}')
return
}
// 1. Reading basic values using value() and type converters
title := doc.value('title').string()
println('Project Title: ${title}')
// 2. Accessing nested tables
owner_name := doc.value('owner.name').string()
org := doc.value('owner.organization').string()
println('Owner: ${owner_name} (${org})')
// 3. Accessing primitive values
server := doc.value('database.server').string()
conn_max := doc.value('database.connection_max').int()
enabled := doc.value('database.enabled').bool()
println('DB Server: ${server} | Connection Max: ${conn_max} | Enabled: ${enabled}')
// 4. Retrieving array values
ports_any := doc.value('database.ports')
println('Ports Any: ${ports_any}')
// Accessing array elements with query syntax
port_0 := doc.value('database.ports[0]').int()
port_1 := doc.value('database.ports[1]').int()
println('Primary Port: ${port_0} | Secondary Port: ${port_1}')
// 5. Using default values for non-existing keys
db_timeout := doc.value('database.timeout').default_to(30).int()
println('Database Timeout (Default): ${db_timeout} seconds')
// 6. Optional retrieval using value_opt()
if db_server := doc.value_opt('database.server') {
println('Optional check: Database server key exists. Value = ${db_server.string()}')
} else {
println('Optional check: Database server key does not exist.')
}
}Strconv
In plain words: The strconv module converts text strings into numbers (atoi, parse_int, parse_uint, atof64) and formats numbers back into custom strings.
This example demonstrates how to convert strings to numbers, parse numbers in different bases and bit-sizes, and convert numbers back to base string representations using the strconv module.
What happens in this example:
- Code note: 1. Convert string to integer types (with error propagation/handling)
- Code note: 2. Parse unsigned and specific bases/bit-sizes
- Code note: parseint(s string, base int, bitsize int) !i64
- Code note: 3. Convert string to float (atof64)
module main
import strconv
fn main() {
println('=== strconv Module Demo ===')
// 1. Convert string to integer types (with error propagation/handling)
val_int := strconv.atoi('12345') or {
println('Error parsing int: ${err}')
0
}
println('Parsed int: ${val_int}')
val_i64 := strconv.atoi64('9223372036854775807') or {
println('Error parsing i64: ${err}')
0
}
println('Parsed i64: ${val_i64}')
// 2. Parse unsigned and specific bases/bit-sizes
// parse_int(s string, base int, bit_size int) !i64
val_hex := strconv.parse_int('0xff', 0, 64) or {
println('Error parsing hex: ${err}')
0
}
println('Parsed hex (0xff in base 0): ${val_hex}')
val_bin := strconv.parse_uint('101010', 2, 32) or {
println('Error parsing binary: ${err}')
0
}
println('Parsed binary (101010 in base 2): ${val_bin}')
// 3. Convert string to float (atof64)
val_f64 := strconv.atof64('3.14159265') or {
println('Error parsing f64: ${err}')
0.0
}
println('Parsed float64: ${val_f64}')
// 4. Convert number to base string representation
// format_int(n i64, radix int) string
binary_str := strconv.format_int(42, 2)
hex_str := strconv.format_int(255, 16)
println('42 in binary: ${binary_str}')
println('255 in hex: ${hex_str}')
}Term
In plain words: The term module adds bold, underline, and bright RGB colors (term.green('Success!'), term.red('Error')) to your terminal output and controls cursor position.
V's standard library provides a direct, cross-platform module named term for querying terminal attributes, altering console text colors/styles, and printing preformatted output badges. Here is the simplest, most practical guide to when you should actually use each of these tools in real-world programming.
1. Terminal Size Metadata
- The Vibe: "Measuring the room size."
- What it does: Tells you how many columns (width) and rows (height) are currently visible in the user's terminal window.
- Best to use when: You are rendering custom terminal layouts, tables, or ASCII art that must fit the screen.
- Real-world example: Wrapping text dynamically so it doesn't spill over the screen edge.
2. ANSI Text Coloring
- The Vibe: "Adding paint to the console."
- What it does: Applies green, red, yellow, or blue styling to terminal characters.
- Best to use when: Highlighting success, errors, warnings, or structural tags.
- Real-world example: Printing error messages in bold red.
3. Styling Modifiers
- The Vibe: "The font options panel."
- What it does: Renders text as bold, underlined, or strikethrough.
- Best to use when: Emphasizing headings, showing links, or marking deprecated options.
- Real-world example: Printing column headers of a CLI table in bold.
4. Background Fills & Combinations
- The Vibe: "The highlighter pen."
- What it does: Fills the text background block with color, and supports mixing foreground styles and background styles.
- Best to use when: Creating alerts, status blocks, or highlighting active menu items.
- Real-world example: Printing a yellow warning status badge with a blue background.
5. Preformatted Messages
- The Vibe: "The instant status stamp."
- What it does: Provides preformatted
[OK],[WARNING], and[FAILED]status boxes with colors built in. - Best to use when: Logging task progress or system bootstrap outputs.
- Real-world example: Printing
[OK] Server started on port 8080.
module main
import term
fn main() {
println('=== term Module Demo ===')
// ==========================================
// 1. Terminal Size Metadata
// ==========================================
// term.get_terminal_size() returns the (width, height) of the active terminal session in columns and rows.
width, height := term.get_terminal_size()
println('Terminal size: ${width} columns x ${height} rows')
// ==========================================
// 2. Colored Text (Foreground Styling)
// ==========================================
// Foreground color helpers wrap the string with ANSI escape codes to change the text color.
println(term.green('This text is green!'))
println(term.red('This text is red!'))
println(term.yellow('This text is yellow!'))
println(term.blue('This text is blue!'))
// ==========================================
// 3. Text Styles & Modifiers
// ==========================================
// Text modifiers add visual decorations like bold, underline, or strikethrough.
println(term.bold('This text is bold!'))
println(term.underline('This text is underlined!'))
println(term.strikethrough('This text has a strikethrough!'))
// ==========================================
// 4. Background Styling
// ==========================================
// Background color helpers fill the background area behind the printed characters.
println(term.bg_blue(' This has a blue background! '))
// ==========================================
// 5. Mixed Styling & Layering
// ==========================================
// We can combine text color, style (bold/underline), and background color by nesting the calls.
println(term.bg_blue(term.yellow(' Yellow text on a blue background ')))
println(term.bg_red(term.white(term.bold(' Bold white text on a red background '))))
println(term.bg_green(term.black(term.underline(' Underlined black text on a green background '))))
// ==========================================
// 6. Preformatted Status Messages
// ==========================================
// V's term module provides built-in preformatted status message helper templates.
// These automatically print colored status stamps like [OK], [WARNING], or [FAILED] followed by the message.
println(term.ok_message('Operation succeeded!'))
println(term.warn_message('This is a warning!'))
println(term.fail_message('Operation failed!'))
}Term Ui
In plain words: term.ui lets you build full-screen interactive terminal apps (TUIs) that respond to keyboard presses, mouse clicks, and window resizing right inside the console!
V's standard library provides the term.ui (or tui) module for building full-featured, cross-platform terminal user interface applications. It manages an event-driven render loop, keyboard and mouse input handling, window resize events, screen buffer clearing, and drawing text, shapes, interactive GUI-in-TUI controls (inputs, buttons, checkboxes, tabs), and custom RGB colors directly in the terminal window.
1. Configuration & Application Lifecycle Callbacks (`Config`)
- The Vibe: "The central app engine configuration and lifecycle setup."
- What it does: Configures the TUI context via
tui.init()with custom application state and lifecycle callback hooks: user_data voidptr: Pointer to custom application state struct, passed to all callbacks.init_fn fn(voidptr): Callback fired once after terminal initialization before the main loop starts.frame_fn fn(voidptr): Main render callback fired automatically atframe_rateframes per second.event_fn fn(&Event, voidptr): Callback fired for keyboard, mouse, and window resize events.cleanup_fn fn(voidptr): Callback fired once when the application exits to restore terminal state cleanly.fail_fn fn(string): Callback fired if a fatal error occurs during initialization.- Options:
frame_rate(FPS, default 30),buffer_size(input buffer size),hide_cursor(hides terminal cursor),capture_events(intercepts raw key combinations such asCtrl+C/Ctrl+Z),window_title(sets window title bar). - Best to use when: Setting up structured terminal applications with state management, custom render rates, and clean shutdown routines.
- Real-world example: Terminal dashboards, file managers, text editors, and system monitoring monitors.
2. Interactive Controls & GUI-in-TUI Patterns
- The Vibe: "Full GUI-style interactive widgets inside the terminal."
- What it does: Implements common UI controls using mouse bounding boxes and keyboard event dispatchers:
- Interactive Text Input Box: Captures focused key strokes (
.utf8input), backspace deletion (.backspace), Enter submission (.enter), and renders a blinking cursor bar (|). - Mouse-Clickable UI Buttons: Renders styled rectangle buttons with hover state (
.mouse_move) and active pressed state (.mouse_down/.mouse_upinside button boundsx..x+w, y..y+h). - Checkboxes & Toggle Switches: Clickable boolean toggle switches (
[X]vs[ ]) for toggling settings like dark mode or grid lines. - Radio Selectors & Segmented Controls: Single-option selection controls (
(โข)vs( )) for choosing modes or speeds. - Navigation Tabs: Tabbed interface bars (
[1] Controls & Form [2] Canvas Drawing [3] Event Log Stream) allowing users to switch active views via macOS-friendly number shortcuts (1,2,3), fallback function keys (F1-F3), or mouse clicks.
3. Graphics & Drawing Primitives
- The Vibe: "The terminal screen canvas paint box."
- What it does: Provides built-in drawing methods for text, lines, and shapes:
draw_text(x, y, text): Renders strings starting at columnx, rowy.draw_point(x, y): Draws a single point/character cell atx,y.draw_line(x1, y1, x2, y2): Draws solid line segments using Bresenham's algorithm or fast horizontal line rendering.draw_dashed_line(x1, y1, x2, y2): Draws dashed line segments.draw_rect(x1, y1, x2, y2): Draws a filled rectangle spanning from top-left(x1, y1)to bottom-right(x2, y2).draw_empty_rect(x1, y1, x2, y2): Draws an outlined rectangle without fill.draw_empty_dashed_rect(x1, y1, x2, y2): Draws a dashed rectangle outline.horizontal_separator(y): Draws a horizontal line rule across the entire window width at rowy.
4. Color & Styling Control (`Color`)
- The Vibe: "The terminal palette and typography controls."
- What it does: Manages foreground/background colors and text attributes:
set_color(Color{r, g, b}): Sets text foreground color (using 24-bit RGB true color or 256-color ANSI fallback).set_bg_color(Color{r, g, b}): Sets background color for text and filled shapes.reset_color()/reset_bg_color(): Restores foreground or background color back to default terminal style.bold(): Enables bold text formatting.reset(): Resets all text styles and color formatting attributes back to default (\x1b[0m).
5. Real-Time Event Loop & Event Stream Inspector (`Event`)
- The Vibe: "The real-time keyboard, mouse, and window event logger."
- What it does: Inspects and logs live input events via
tui.Event: typ EventType:.key_down,.mouse_down,.mouse_up,.mouse_move,.mouse_drag,.mouse_scroll,.resized.- Keyboard details:
code(KeyCodeenum matching.escape,.enter,.space,.up,.down,.left,.right,.tab,._1โ._9, letters, numbers),modifiers(.ctrl,.shift,.altflags),ascii,utf8. - Mouse details:
x,ycoordinates,button(.left,.middle,.right),direction(.up,.down,.left,.rightscroll). - Window resize details:
width,heightpassed on.resizedevents (and auto-updated inwindow_width/window_height).
module main
// Import the terminal UI module from V's standard library
import term.ui as tui
// Button represents an interactive mouse-clickable TUI button
struct Button {
id string
label string
x int
y int
width int
height int
}
// App struct stores complete application state across render frames and events
struct App {
mut:
tui &tui.Context = unsafe { nil }
// Navigation Tabs
active_tab int // 0: Form & Controls, 1: Drawing Primitives, 2: Event Stream Log
tab_titles []string
// Form & Widget State
text_input string
input_focused bool
counter int
show_grid bool
dark_mode bool
selected_option int
radio_options []string
// Buttons list
buttons []Button
// Event Stream Log (last 10 events)
event_log []string
// Mouse tracking
mouse_x int
mouse_y int
hovered_btn string
clicked_btn string
scroll_state string
}
// log_event adds a formatted message to the event log buffer
fn (mut app App) log_event(msg string) {
app.event_log << msg
if app.event_log.len > 10 {
app.event_log.delete(0)
}
}
// frame_fn is called automatically on every render cycle (at 30 FPS)
fn frame_fn(x voidptr) {
mut app := unsafe { &App(x) }
// 1. Clear previous frame contents from screen buffer
app.tui.clear()
// 2. Main Color Scheme depending on Dark Mode toggle
bg_r, bg_g, bg_b := if app.dark_mode { u8(15), u8(18), u8(28) } else { u8(30), u8(45), u8(70) }
accent_r, accent_g, accent_b := u8(0), u8(180), u8(220)
// 3. Render Top Navigation Bar & Tabs
app.tui.set_bg_color(r: bg_r, g: bg_g, b: bg_b)
app.tui.set_color(r: 255, g: 255, b: 255)
app.tui.bold()
header := ' === V term.ui Interactive Widgets & Event Inspector === [Res: ${app.tui.window_width}x${app.tui.window_height}] '
app.tui.draw_text(2, 1, header)
app.tui.reset()
// Draw Tab Buttons (using macOS-friendly standard 1, 2, 3 shortcuts)
mut tab_x := 4
for i in 0 .. app.tab_titles.len {
title := app.tab_titles[i]
if i == app.active_tab {
app.tui.set_bg_color(r: accent_r, g: accent_g, b: accent_b)
app.tui.set_color(r: 255, g: 255, b: 255)
app.tui.bold()
} else {
app.tui.set_bg_color(r: 60, g: 70, b: 90)
app.tui.set_color(r: 200, g: 200, b: 200)
}
tab_btn_text := ' [ ${i + 1} ] ${title} '
app.tui.draw_text(tab_x, 3, tab_btn_text)
app.tui.reset()
tab_x += tab_btn_text.len + 2
}
app.tui.horizontal_separator(4)
// 4. Render Active Tab Content
match app.active_tab {
0 {
// ==========================================
// TAB 0: Interactive Form, Textbox & Buttons
// ==========================================
app.tui.set_color(r: 255, g: 220, b: 0)
app.tui.bold()
app.tui.draw_text(4, 6, '1. Interactive Text Input Box (Click or Press TAB to focus)')
app.tui.reset()
// Textbox Container
input_bg_r, input_bg_g, input_bg_b := if app.input_focused {
u8(40), u8(60), u8(100)
} else {
u8(25), u8(30), u8(45)
}
app.tui.set_bg_color(r: input_bg_r, g: input_bg_g, b: input_bg_b)
app.tui.set_color(r: 255, g: 255, b: 255)
app.tui.draw_rect(4, 7, 54, 9)
cursor_char := if app.input_focused && (app.tui.frame_count / 15) % 2 == 0 {
'|'
} else {
''
}
display_text := if app.text_input == '' {
'Type text here...'
} else {
app.text_input
}
app.tui.draw_text(6, 8, '> ${display_text}${cursor_char}')
app.tui.reset()
// Interactive Buttons Section
app.tui.set_color(r: 255, g: 220, b: 0)
app.tui.bold()
app.tui.draw_text(4, 11, '2. Clickable UI Buttons & Counter State')
app.tui.reset()
// Render Buttons
for btn in app.buttons {
is_hover := app.hovered_btn == btn.id
is_click := app.clicked_btn == btn.id
b_r, b_g, b_b := if is_click {
u8(255), u8(140), u8(0)
} else if is_hover {
u8(0), u8(150), u8(220)
} else {
u8(50), u8(70), u8(100)
}
app.tui.set_bg_color(r: b_r, g: b_g, b: b_b)
app.tui.set_color(r: 255, g: 255, b: 255)
app.tui.bold()
app.tui.draw_rect(btn.x, btn.y, btn.x + btn.width, btn.y + btn.height)
app.tui.draw_text(btn.x + 2, btn.y + 1, btn.label)
app.tui.reset()
}
// Display Counter Value
app.tui.set_color(r: 0, g: 255, b: 180)
app.tui.bold()
app.tui.draw_text(4, 15, 'Current Counter Value: ${app.counter}')
app.tui.reset()
// Checkboxes & Radio Selectors Section
app.tui.set_color(r: 255, g: 220, b: 0)
app.tui.bold()
app.tui.draw_text(4, 17, '3. Checkbox & Radio Controls')
app.tui.reset()
chk_grid := if app.show_grid { '[X]' } else { '[ ]' }
chk_dark := if app.dark_mode { '[X]' } else { '[ ]' }
app.tui.draw_text(4, 18, '${chk_grid} Show Grid (Click to toggle)')
app.tui.draw_text(32, 18, '${chk_dark} Dark Mode Theme (Click to toggle)')
app.tui.draw_text(4, 20, 'Select Speed Mode:')
for idx, opt in app.radio_options {
selected_str := if idx == app.selected_option { '(โข)' } else { '( )' }
app.tui.draw_text(4 + idx * 16, 21, '${selected_str} ${opt}')
}
}
1 {
// ==========================================
// TAB 1: Graphics & Drawing Canvas
// ==========================================
app.tui.set_color(r: 0, g: 220, b: 255)
app.tui.bold()
app.tui.draw_text(4, 6, '=== Canvas Graphics & Drawing Primitives ===')
app.tui.reset()
// Filled Rect
app.tui.set_bg_color(r: 180, g: 40, b: 80)
app.tui.draw_rect(4, 8, 30, 12)
app.tui.reset_bg_color()
app.tui.set_color(r: 255, g: 255, b: 255)
app.tui.draw_text(6, 10, 'Filled Rect (draw_rect)')
// Outline Rect
app.tui.set_color(r: 0, g: 255, b: 150)
app.tui.draw_empty_rect(34, 8, 60, 12)
app.tui.draw_text(36, 10, 'Outline Rect (draw_empty_rect)')
// Dashed Line & Dashed Rect
app.tui.set_color(r: 255, g: 200, b: 0)
app.tui.draw_dashed_line(4, 14, 30, 14)
app.tui.draw_text(4, 15, 'Dashed Line (draw_dashed_line)')
app.tui.draw_empty_dashed_rect(34, 14, 60, 17)
app.tui.draw_text(36, 15, 'Dashed Rect (draw_empty_dashed_rect)')
app.tui.reset()
}
else {
// ==========================================
// TAB 2: Real-time Event Log Inspector
// ==========================================
app.tui.set_color(r: 255, g: 180, b: 0)
app.tui.bold()
app.tui.draw_text(4, 6, '=== Live Input Event Stream (Last 10 Events) ===')
app.tui.reset()
app.tui.draw_empty_rect(4, 7, 85, 19)
for i, log_entry in app.event_log {
app.tui.set_color(r: 200, g: 220, b: 255)
app.tui.draw_text(6, 8 + i, '[#${i + 1}] ${log_entry}')
}
app.tui.reset()
}
}
// 5. Footer Status & macOS-friendly Shortcuts
app.tui.horizontal_separator(21)
app.tui.set_color(r: 180, g: 180, b: 180)
app.tui.draw_text(4, 22, 'Mouse Pos: X=${app.mouse_x}, Y=${app.mouse_y} | Scroll: ${app.scroll_state} | Active Hover: "${app.hovered_btn}"')
app.tui.draw_text(4, 23, 'Shortcuts: [1-3] Switch Tabs | [TAB] Focus Textbox | [ESC] or "q" Quit')
app.tui.reset()
app.tui.set_cursor_position(0, 0)
app.tui.reset()
app.tui.flush()
}
// event_fn handles keyboard, mouse, and window resize events
fn event_fn(e &tui.Event, x voidptr) {
mut app := unsafe { &App(x) }
match e.typ {
.key_down {
app.log_event('Key Down: Code=${e.code} (${int(e.code)}) | Modifiers=${e.modifiers} | Utf8="${e.utf8}"')
// Mac-friendly Tab switching shortcuts (1, 2, 3 or Escape/q for quit)
match e.code {
.escape, .q {
if !app.input_focused || e.code == .escape {
exit(0)
}
}
.tab {
app.input_focused = !app.input_focused
}
._1, .f1 {
if !app.input_focused || e.code == .f1 {
app.active_tab = 0
}
}
._2, .f2 {
if !app.input_focused || e.code == .f2 {
app.active_tab = 1
}
}
._3, .f3 {
if !app.input_focused || e.code == .f3 {
app.active_tab = 2
}
}
else {}
}
// Textbox Input Editing
if app.input_focused {
match e.code {
.backspace {
if app.text_input.len > 0 {
app.text_input = app.text_input[..app.text_input.len - 1]
}
}
.enter {
app.log_event('Submitted Text: "${app.text_input}"')
}
else {
if e.utf8.len > 0 && e.code != .tab && e.code != .escape {
app.text_input += e.utf8
}
}
}
}
}
.mouse_move {
app.mouse_x = e.x
app.mouse_y = e.y
// Detect button hover
mut found_hover := ''
for btn in app.buttons {
if app.active_tab == 0 && e.x >= btn.x && e.x <= btn.x + btn.width
&& e.y >= btn.y && e.y <= btn.y + btn.height {
found_hover = btn.id
break
}
}
app.hovered_btn = found_hover
}
.mouse_down {
app.mouse_x = e.x
app.mouse_y = e.y
app.log_event('Mouse Click: Btn=${e.button} at (${e.x}, ${e.y})')
// 1. Check Tab Clicks
if e.y == 3 {
if e.x >= 4 && e.x <= 18 {
app.active_tab = 0
} else if e.x >= 20 && e.x <= 36 {
app.active_tab = 1
} else if e.x >= 38 && e.x <= 56 {
app.active_tab = 2
}
}
// 2. Check Textbox Focus Click
if app.active_tab == 0 && e.x >= 4 && e.x <= 54 && e.y >= 7 && e.y <= 9 {
app.input_focused = true
} else if app.active_tab == 0 && (e.y < 7 || e.y > 9) {
app.input_focused = false
}
// 3. Check Button Clicks
if app.active_tab == 0 {
for btn in app.buttons {
if e.x >= btn.x && e.x <= btn.x + btn.width && e.y >= btn.y
&& e.y <= btn.y + btn.height {
app.clicked_btn = btn.id
match btn.id {
'inc' {
app.counter++
app.log_event('Button Click: Counter Incremented to ${app.counter}')
}
'dec' {
app.counter--
app.log_event('Button Click: Counter Decremented to ${app.counter}')
}
'clear' {
app.text_input = ''
app.log_event('Button Click: Text Input Cleared')
}
'reset' {
app.counter = 0
app.log_event('Button Click: Counter Reset to 0')
}
else {}
}
break
}
}
// Checkbox Toggles
if e.y == 18 {
if e.x >= 4 && e.x <= 20 {
app.show_grid = !app.show_grid
app.log_event('Toggle Grid: ${app.show_grid}')
} else if e.x >= 32 && e.x <= 52 {
app.dark_mode = !app.dark_mode
app.log_event('Toggle Dark Mode: ${app.dark_mode}')
}
}
// Radio Options
if e.y == 21 {
if e.x >= 4 && e.x <= 16 {
app.selected_option = 0
app.log_event('Selected Speed: Slow')
} else if e.x >= 20 && e.x <= 32 {
app.selected_option = 1
app.log_event('Selected Speed: Normal')
} else if e.x >= 36 && e.x <= 48 {
app.selected_option = 2
app.log_event('Selected Speed: Fast')
}
}
}
}
.mouse_up {
app.clicked_btn = ''
}
.mouse_scroll {
app.scroll_state = '${e.direction}'
app.log_event('Mouse Scroll: Direction=${e.direction} at (${e.x}, ${e.y})')
}
.resized {
app.log_event('Window Resized: Width=${app.tui.window_width}, Height=${app.tui.window_height}')
}
else {}
}
}
fn main() {
mut app := &App{
tab_titles: ['Controls & Form', 'Canvas Drawing', 'Event Log Stream']
text_input: 'Hello Vlang term.ui!'
counter: 10
show_grid: true
dark_mode: true
radio_options: ['Slow', 'Normal', 'Fast']
buttons: [
Button{
id: 'inc'
label: '[ + ] Increment'
x: 4
y: 12
width: 16
height: 2
},
Button{
id: 'dec'
label: '[ - ] Decrement'
x: 22
y: 12
width: 16
height: 2
},
Button{
id: 'reset'
label: '[ R ] Reset'
x: 40
y: 12
width: 12
height: 2
},
Button{
id: 'clear'
label: '[ C ] Clear Text'
x: 54
y: 12
width: 16
height: 2
},
]
}
app.tui = tui.init(
user_data: app
frame_fn: frame_fn
event_fn: event_fn
window_title: 'V Comprehensive Terminal GUI & Widgets'
hide_cursor: true
capture_events: true
frame_rate: 30
buffer_size: 256
)
app.tui.run()!
}Benchmark
In plain words: The benchmark module times how many milliseconds or microseconds each step of your code takes so you can measure performance improvements accurately.
This example demonstrates timing code execution chunks and step-by-step progress benchmarking using the benchmark module.
What happens in this example:
- Code note: Example 1: Using benchmark.start() and measure()
- Code note: Simulate work chunk 1
- Code note: Simulate work chunk 2
- Code note: Example 2: Using structured new_benchmark()
module main
import benchmark
import time
fn main() {
println('=== benchmark Module Demo ===')
// Example 1: Using benchmark.start() and measure()
println('--- Simple Measurement ---')
mut b := benchmark.start()
// Simulate work chunk 1
time.sleep(50 * time.millisecond)
b.measure('Simulated task 1 (50ms sleep)')
// Simulate work chunk 2
time.sleep(100 * time.millisecond)
b.measure('Simulated task 2 (100ms sleep)')
// Example 2: Using structured new_benchmark()
println('\n--- Structured Step-by-Step Benchmarking ---')
mut bmark := benchmark.new_benchmark()
// Step 1: Ok step
bmark.step()
time.sleep(30 * time.millisecond)
bmark.ok()
println(bmark.step_message('Step 1 (successful arithmetic)'))
// Step 2: Failed step demo
bmark.step()
time.sleep(10 * time.millisecond)
bmark.fail()
println(bmark.step_message('Step 2 (simulated failure verification)'))
// Finalize and print results summary
bmark.stop()
println(bmark.total_message('Final summary of execution stages'))
}Clipboard
In plain words: The clipboard module lets your V program copy text toโor paste text fromโyour computer's system clipboard (Ctrl+C / Cmd+V).
This example demonstrates writing to and reading from the system clipboard on macOS using the clipboard module, including a backup and restore mechanism.
What happens in this example:
- Code note: 1. Initialize clipboard
- Code note: 2. Backup current clipboard content so we do not overwrite user data permanently
- Code note: 3. Copy new text to clipboard
- Code note: 4. Paste back to verify
module main
import clipboard
fn main() {
println('=== clipboard Module Demo ===')
// 1. Initialize clipboard
mut cb := clipboard.new()
defer {
cb.destroy()
}
if !cb.is_available() {
println('Clipboard is not available on this platform/session.')
return
}
// 2. Backup current clipboard content so we do not overwrite user data permanently
original_text := cb.paste()
println('Backed up original clipboard text (length: ${original_text.len})')
// 3. Copy new text to clipboard
test_message := 'Hello from Vlang standard library!'
println('Copying text to clipboard: "${test_message}"')
if cb.copy(test_message) {
println('Text successfully copied!')
} else {
println('Failed to copy text.')
}
// 4. Paste back to verify
pasted_text := cb.paste()
println('Pasted text from clipboard: "${pasted_text}"')
// 5. Restore original clipboard content
println('Restoring original clipboard content...')
cb.copy(original_text)
println('Clipboard restore completed successfully.')
}Semver
In plain words: The semver module parses software version numbers like 1.2.3-beta and checks version rules like >=1.0.0 <2.0.0.
This example demonstrates parsing semantic versions and checking constraint satisfaction using the semver module.
What happens in this example:
- Code note: 1. Parsing semver strings
- Code note: 2. Accessing parts of the version struct
- Code note: 3. Comparisons using relational operators
- Code note: 4. Checking against version ranges (constraints)
module main
import semver
fn main() {
println('=== semver Module Demo ===')
// 1. Parsing semver strings
v1 := semver.from('1.5.0-beta.1+build.123') or {
println('Failed to parse version: ${err}')
return
}
v2 := semver.from('1.5.0') or {
println('Failed to parse version: ${err}')
return
}
v3 := semver.from('2.0.0-rc.1') or {
println('Failed to parse version: ${err}')
return
}
// 2. Accessing parts of the version struct
println('Version 1 components:')
println(' Raw string: ${v1.str()}')
println(' Major: ${v1.major}')
println(' Minor: ${v1.minor}')
println(' Patch: ${v1.patch}')
println(' Prerelease: ${v1.prerelease}')
println(' Build info: ${v1.metadata}')
// 3. Comparisons using relational operators
println('\nVersion Comparisons:')
println(' ${v1} < ${v2} ? -> ${v1 < v2}')
println(' ${v2} > ${v1} ? -> ${v2 > v1}')
println(' ${v3} >= ${v2} ? -> ${v3 >= v2}')
// 4. Checking against version ranges (constraints)
println('\nVersion Constraint Satisfactions:')
// Range checking
println(' Is ${v2} in range ">=1.0.0 <2.0.0" ? -> ${v2.satisfies('>=1.0.0 <2.0.0')}')
println(' Is ${v3} in range ">=1.0.0 <2.0.0" ? -> ${v3.satisfies('>=1.0.0 <2.0.0')}')
// Complex constraint checking using logical OR (||)
range_query := '^1.4.0 || >=2.0.0'
println(' Does ${v2} satisfy "${range_query}"? -> ${v2.satisfies(range_query)}')
println(' Does ${v3} satisfy "${range_query}"? -> ${v3.satisfies(range_query)}')
}Maps Standard Library Module (maps.v)
In plain words: Demonstrates the standard library maps helper functions: maps.filter, maps.to_array, maps.from_array, maps.invert, and maps.merge.
This example demonstrates the high-level helpers in V's maps module, including filtering, transforming, inverting, merging, and converting between maps and arrays. The repository version is a verified walkthrough that exercises the full set of documented helpers.
module main
import maps
fn main() {
println('=== maps Module Demo ===')
m1 := {
'apple': 1
'banana': 2
'cherry': 3
}
filtered := maps.filter(m1, fn (k string, v int) bool {
return v > 1
})
println('filter(): ${filtered}')
keys_upper := maps.to_array(m1, fn (k string, v int) string {
return k.to_upper()
})
println('to_array(): ${keys_upper}')
inverted := maps.invert(m1)
println('invert(): ${inverted}')
fruits := ['apple', 'banana', 'cherry']
map_from_arr := maps.from_array(fruits)
println('from_array(): ${map_from_arr}')
m2 := {
'banana': 20
'date': 4
}
merged := maps.merge(m1, m2)
println('merge(): ${merged}')
mut mut_map := {
'a': 1
}
maps.merge_in_place(mut mut_map, {
'b': 2
'c': 3
})
println('merge_in_place(): ${mut_map}')
flat_items := maps.flat_map[string, int, string](m1, fn (k string, v int) []string {
return [k, v.str()]
})
println('flat_map(): ${flat_items}')
transformed := maps.to_map[string, int, string, int](m1, fn (k string, v int) (string, int) {
return k.to_upper(), v * 10
})
println('to_map(): ${transformed}')
}Context
In plain words: A context.Context carries a deadline, timeout, or cancellation signal across multiple functions and background threads so slow requests can be stopped cleanly.
This example demonstrates propagating request-scoped values, cancellation signals, and timeouts across thread boundaries using the context module.
What happens in this example:
- Code note: 1. Context with Value
- Code note: Useful for passing metadata (e.g. Request ID) through call chains
- Code note: 2. Context with Cancellation
- Code note: Check if canceled
module main
import context
import time
fn main() {
println('=== context Module Demo ===')
// 1. Context with Value
// Useful for passing metadata (e.g. Request ID) through call chains
mut ctx_bg := context.background()
mut ctx_val := context.with_value(ctx_bg, 'request_id', 'REQ-101')
if req_id := ctx_val.value('request_id') {
if req_id is string {
println('Request ID in context: ${req_id}')
}
}
// 2. Context with Cancellation
mut ctx_cancel, cancel := context.with_cancel(mut ctx_val)
// Check if canceled
println('Before cancel - Done channel is open')
cancel() // trigger cancellation
// Select block to read from done channel
done_ch := ctx_cancel.done()
select {
_ := <-done_ch {
println('Context cancellation detected successfully!')
}
1 * time.second {
println('Timeout waiting for cancellation.')
}
}
// 3. Context with Timeout
// Abandon execution after a duration
mut ctx_timeout, cancel_timeout := context.with_timeout(mut ctx_bg, 50 * time.millisecond)
defer {
cancel_timeout()
}
println('Waiting for context timeout (50ms)...')
start := time.now()
timeout_ch := ctx_timeout.done()
select {
_ := <-timeout_ch {
elapsed := time.since(start)
println('Timeout triggered after ${elapsed.milliseconds()} ms!')
}
1 * time.second {
println('Error: Timeout did not trigger in time.')
}
}
}Archive Tar
In plain words: Creates and reads .tar and .tar.gz archive bundles right in memory or on disk using archive.tar and compress.gzip.
This example demonstrates reading and inspecting the contents of .tar.gz files using the archive.tar module.
What happens in this example:
- Code note: CustomReader implements the tar.Reader interface
- Code note: Trim content bytes to display them cleanly
- Code note: Ignore details for this demo
- Code note: 1. Create a dummy file to archive
module main
import archive.tar
import os
// CustomReader implements the tar.Reader interface
struct CustomReader {
pub mut:
files_found int
}
fn (mut cr CustomReader) dir_block(mut read tar.Read, size u64) {
println('Directory in tar: ${read.get_path()}')
}
fn (mut cr CustomReader) file_block(mut read tar.Read, size u64) {
println('File in tar: ${read.get_path()} (${size} bytes)')
cr.files_found++
}
fn (mut cr CustomReader) data_block(mut read tar.Read, data []u8, pending int) {
// Trim content bytes to display them cleanly
content := data.bytestr().trim_space()
println(' Content snippet: "${content}"')
}
fn (mut cr CustomReader) other_block(mut read tar.Read, details string) {
// Ignore details for this demo
}
fn main() {
println('=== archive.tar Module Demo ===')
// 1. Create a dummy file to archive
temp_file := 'temp_file_for_tar.txt'
os.write_file(temp_file, 'Hello standard archive tar from Vlang!') or {
println('Failed to write temp file: ${err}')
return
}
defer {
os.rm(temp_file) or {}
}
// 2. Create the tar.gz archive using system tar
tar_archive := 'temp_archive.tar.gz'
println('Creating tar archive using system tar...')
tar_cmd := if os.user_os() == 'macos' { 'COPYFILE_DISABLE=1 tar -czf' } else { 'tar -czf' }
os.execute('${tar_cmd} ${tar_archive} ${temp_file}')
defer {
os.rm(tar_archive) or {}
}
// 3. Read and parse the tar.gz archive using V's archive.tar module
println('Reading archive using vlib/archive/tar:')
mut reader := CustomReader{}
// Read and parse
tar.read_tar_gz_file(tar_archive, reader) or {
println('Failed to read tar archive: ${err}')
return
}
println('Total files found in archive: ${reader.files_found}')
}Compress Deflate
In plain words: Shrinks data to a fraction of its size with deflate.compress(...) and restores the exact original bytes with deflate.decompress(...).
This example demonstrates standard Deflate byte stream compression and decompression using the compress.deflate module.
What happens in this example:
- Code note: 1. Data to compress
- Code note: 2. Compress the data using deflate.compress
- Code note: 3. Decompress the data using deflate.decompress
- Code note: 4. Verify the result
module main
import compress.deflate
fn main() {
println('=== compress.deflate Module Demo ===')
// 1. Data to compress
original_text := 'V programming language standard library deflate compression demo. Deflate is a lossless data compression algorithm.'
println('Original Text length: ${original_text.len} bytes')
// 2. Compress the data using deflate.compress
compressed_bytes := deflate.compress(original_text.bytes()) or {
println('Compression failed: ${err}')
return
}
println('Compressed size: ${compressed_bytes.len} bytes')
// 3. Decompress the data using deflate.decompress
decompressed_bytes := deflate.decompress(compressed_bytes) or {
println('Decompression failed: ${err}')
return
}
println('Decompressed size: ${decompressed_bytes.len} bytes')
// 4. Verify the result
decompressed_text := decompressed_bytes.bytestr()
println('Decompressed text equals original? -> ${decompressed_text == original_text}')
println('Decompressed Text: "${decompressed_text}"')
}Compress Gzip
In plain words: Compresses and decompresses .gz (Gzip) data streamsโthe standard compression format used across web servers and .tar.gz downloads.
This example demonstrates compressing and decompressing binary or text data using the compress.gzip module.
What happens in this example:
- Code note: 1. Original text data
- Code note: 2. Compress the data
- Code note: 3. Decompress the data
- Code note: 4. Convert back to string and verify
module main
import compress.gzip
fn main() {
println('=== compress.gzip Module Demo ===')
// 1. Original text data
original_text := 'V programming language standard library gzip compression and decompression demonstration. This string is long enough to show compression.'
println('Original Text length: ${original_text.len} bytes')
// 2. Compress the data
compressed_bytes := gzip.compress(original_text.bytes()) or {
println('Compression failed: ${err}')
return
}
println('Compressed size: ${compressed_bytes.len} bytes')
// 3. Decompress the data
decompressed_bytes := gzip.decompress(compressed_bytes) or {
println('Decompression failed: ${err}')
return
}
println('Decompressed size: ${decompressed_bytes.len} bytes')
// 4. Convert back to string and verify
decompressed_text := decompressed_bytes.bytestr()
println('Decompressed text equals original? -> ${decompressed_text == original_text}')
println('Decompressed Text: "${decompressed_text}"')
}Compress Szip
In plain words: Works with .zip archives using V's built-in compress.szip module to bundle and extract compressed files.
This example demonstrates packaging multiple files into zip archives recursively, inspecting zip contents/meta-data (size, CRC32), and extracting zip files to folders using the compress.szip module.
module main
import os
import compress.szip
fn main() {
println('=== compress.szip Module Demo ===')
zip_filename := 'demo_archive.zip'
dest_dir := 'extracted_demo'
// Ensure cleanup of any temp files/folders
defer {
os.rm(zip_filename) or {}
os.rmdir_all(dest_dir) or {}
println('\nCleaned up archive and extraction folder.')
}
println('\n--- 1. Creating a Zip Archive ---')
// Open a new zip archive for writing (creating it)
mut archive := szip.open(zip_filename, .default_compression, .write) or {
println('Failed to create zip: ${err}')
return
}
// Add first file entry
archive.open_entry('first_file.txt') or {
println('Failed to open entry: ${err}')
return
}
archive.write_entry('Hello from the first file inside our zip archive!'.bytes()) or {
println('Failed to write entry: ${err}')
return
}
archive.close_entry()
// Add second file entry
archive.open_entry('docs/second_file.txt') or {
println('Failed to open entry: ${err}')
return
}
archive.write_entry('This is a second file nested inside a docs directory.'.bytes()) or {
println('Failed to write entry: ${err}')
return
}
archive.close_entry()
// Close the zip file
archive.close()
println('Successfully created zip archive "${zip_filename}" with 2 entries.')
println('\n--- 2. Inspecting the Zip Archive ---')
// Open zip file in read-only mode to inspect its contents
mut reader := szip.open(zip_filename, .default_compression, .read_only) or {
println('Failed to open zip for reading: ${err}')
return
}
total_entries := reader.total() or { 0 }
println('Total entries found in zip: ${total_entries}')
// Inspect first entry details
reader.open_entry_by_index(0) or {
println('Failed to open entry 0: ${err}')
return
}
name := reader.name()
size := reader.size()
crc := reader.crc32()
println('Entry 0 details -> Name: "${name}", Size: ${size} bytes, CRC32: ${crc}')
reader.close_entry()
reader.close()
println('\n--- 3. Extracting Zip Archive contents to Directory ---')
os.mkdir(dest_dir) or {
println('Failed to create destination directory: ${err}')
return
}
// Extract the full archive to the target folder
success := szip.extract_zip_to_dir(zip_filename, dest_dir) or {
println('Extraction failed: ${err}')
return
}
if success {
println('Successfully extracted all entries to folder "${dest_dir}".')
// Read and display content from extracted files
file1_content := os.read_file(os.join_path(dest_dir, 'first_file.txt')) or { '' }
file2_content := os.read_file(os.join_path(dest_dir, 'docs', 'second_file.txt')) or { '' }
println('Extracted first_file.txt: "${file1_content}"')
println('Extracted docs/second_file.txt: "${file2_content}"')
} else {
println('Extraction reported failure.')
}
}Compress Zlib
In plain words: Compresses and decompresses byte arrays using the industry-standard zlib format (used inside PNG images and Git objects).
This example demonstrates standard Zlib byte stream compression and decompression using the compress.zlib module.
What happens in this example:
- Code note: 1. Data to compress
- Code note: 2. Compress the data using zlib.compress
- Code note: 3. Decompress the data using zlib.decompress
- Code note: 4. Verify the result
module main
import compress.zlib
fn main() {
println('=== compress.zlib Module Demo ===')
// 1. Data to compress
original_text := 'V programming language standard library zlib compression demo. Zlib uses the deflate algorithm with headers and checksum.'
println('Original Text length: ${original_text.len} bytes')
// 2. Compress the data using zlib.compress
compressed_bytes := zlib.compress(original_text.bytes()) or {
println('Compression failed: ${err}')
return
}
println('Compressed size: ${compressed_bytes.len} bytes')
// 3. Decompress the data using zlib.decompress
decompressed_bytes := zlib.decompress(compressed_bytes) or {
println('Decompression failed: ${err}')
return
}
println('Decompressed size: ${decompressed_bytes.len} bytes')
// 4. Verify the result
decompressed_text := decompressed_bytes.bytestr()
println('Decompressed text equals original? -> ${decompressed_text == original_text}')
println('Decompressed Text: "${decompressed_text}"')
}Compress Zstd
In plain words: Uses the ultra-fast Zstandard (zstd) algorithm to compress and decompress data at high speed.
This example demonstrates Zstd compression and decompression using the fast Facebook Zstandard algorithm in the compress.zstd module.
What happens in this example:
- Code note: 1. Check version details
- Code note: 2. Data to compress
- Code note: 3. Compress using zstd.compress (specifying standard parameters)
- Code note: 4. Decompress using zstd.decompress
module main
import compress.zstd
fn main() {
println('=== compress.zstd Module Demo ===')
// 1. Check version details
version := zstd.version_string()
println('ZSTD Library Version: ${version}')
// 2. Data to compress
original_text := 'Zstd, short for Zstandard, is a fast lossless compression algorithm developed by Facebook. It offers high compression ratios.'
println('\nOriginal Text length: ${original_text.len} bytes')
// 3. Compress using zstd.compress (specifying standard parameters)
compressed_bytes := zstd.compress(original_text.bytes(), compression_level: 3) or {
println('Compression failed: ${err}')
return
}
println('Compressed size: ${compressed_bytes.len} bytes')
// 4. Decompress using zstd.decompress
decompressed_bytes := zstd.decompress(compressed_bytes) or {
println('Decompression failed: ${err}')
return
}
println('Decompressed size: ${decompressed_bytes.len} bytes')
// 5. Verify and display result
decompressed_text := decompressed_bytes.bytestr()
println('Decompressed text equals original? -> ${decompressed_text == original_text}')
println('Decompressed Text: "${decompressed_text}"')
}Io Fs
In plain words: Demonstrates file-system abstraction interfaces for working with files and folders cleanly.
This example demonstrates how os.File implements io.Reader and io.Writer interfaces, allowing standard file operations to utilize stream-oriented utilities like io.cp and io.BufferedReader.
What happens in this example:
- Code note: Ensure files are cleaned up on completion
- Code note: os.create returns an os.File struct, which implements the io.Writer interface.
- Code note: Write data using the io.Writer write() method
- Code note: os.open opens a file for reading, returning an os.File (which implements io.Reader).
module main
import os
import io
fn main() {
println('=== io & File System (os.File) Demo ===')
src_path := 'temp_src_file.txt'
dst_path := 'temp_dst_file.txt'
// Ensure files are cleaned up on completion
defer {
os.rm(src_path) or {}
os.rm(dst_path) or {}
println('\nCleaned up temporary files.')
}
println('\n--- 1. Creating and Writing to File via io.Writer ---')
// os.create returns an os.File struct, which implements the io.Writer interface.
mut src_file := os.create(src_path) or {
println('Failed to create file: ${err}')
return
}
// Write data using the io.Writer write() method
content_to_write := 'Hello! This is a file system demo.\nIt demonstrates how os.File integrates with the io module.\n'
written_bytes := src_file.write(content_to_write.bytes()) or {
println('Failed to write to file: ${err}')
return
}
println('Wrote ${written_bytes} bytes to "${src_path}" using the io.Writer interface.')
src_file.close()
println('\n--- 2. Reading from File via io.BufferedReader ---')
// os.open opens a file for reading, returning an os.File (which implements io.Reader).
mut read_file := os.open(src_path) or {
println('Failed to open file: ${err}')
return
}
// Wrap os.File in io.BufferedReader for convenient line-by-line reading
mut buf_reader := io.new_buffered_reader(reader: read_file)
// Read lines until EOF
for {
line := buf_reader.read_line() or { break }
println('Buffered Read Line: "${line}"')
}
read_file.close()
println('\n--- 3. Copying File Contents using io.cp ---')
// Re-open source file for reading (implements io.Reader)
mut src_to_copy := os.open(src_path) or {
println('Failed to open source file: ${err}')
return
}
defer { src_to_copy.close() }
// Create a new destination file for writing (implements io.Writer)
mut dst_file := os.create(dst_path) or {
println('Failed to create destination file: ${err}')
return
}
defer { dst_file.close() }
// Copy all contents from reader to writer using io.cp
io.cp(mut src_to_copy, mut dst_file) or {
println('Failed to copy file content: ${err}')
return
}
// Explicitly close files so data is flushed and readable
src_to_copy.close()
dst_file.close()
println('Copied content from "${src_path}" to "${dst_path}" via io.cp.')
// Verify the destination file contents
copied_content := os.read_file(dst_path) or {
println('Failed to read destination file: ${err}')
return
}
println('Copied File Contents:\n${copied_content.trim_space()}')
}Io
In plain words: Shows how V's io.Reader and io.Writer interfaces let functions stream bytes from any source to any destination using io.cp(...).
This example demonstrates implementing custom Reader and Writer structs and using the io.cp utility to copy data between streams using the io module.
What happens in this example:
- Code note: SimpleReader implements the io.Reader interface
- Code note: Return io.Eof when the end of the stream is reached
- Code note: SimpleWriter implements the io.Writer interface
- Code note: 1. Initialize Reader and Writer
module main
import io
// SimpleReader implements the io.Reader interface
struct SimpleReader {
data string
mut:
pos int
}
fn (mut sr SimpleReader) read(mut buf []u8) !int {
if sr.pos >= sr.data.len {
// Return io.Eof when the end of the stream is reached
return io.Eof{}
}
mut bytes_read := 0
for sr.pos < sr.data.len && bytes_read < buf.len {
buf[bytes_read] = sr.data[sr.pos]
sr.pos++
bytes_read++
}
return bytes_read
}
// SimpleWriter implements the io.Writer interface
struct SimpleWriter {
mut:
buf []u8
}
fn (mut sw SimpleWriter) write(buf []u8) !int {
sw.buf << buf
return buf.len
}
fn main() {
println('=== io Module Demo ===')
// 1. Initialize Reader and Writer
mut reader := SimpleReader{
data: 'Vlang standard library: io package demo.'
}
mut writer := SimpleWriter{}
// 2. Use io.cp to copy data from Reader to Writer
println('Copying data from custom Reader to custom Writer via io.cp...')
io.cp(mut reader, mut writer) or {
println('Error copying data: ${err}')
return
}
// 3. Print the written data
written_str := writer.buf.bytestr()
println('Writer received: "${written_str}"')
}Io Util
In plain words: Uses io.util to safely create temporary files and folders (util.temp_file, util.temp_dir) that won't clash with existing files.
This example demonstrates using the io.util module for creating, writing to, reading from, and cleaning up temporary files and directories.
What happens in this example:
- Code note: Create a temporary file. The '' character in the pattern is replaced by a random number.*
- Code note: tfo.path defaults to the system temp directory if not specified.
- Code note: Close the returned file handle immediately so we can open it with a clean write mode
- Code note: Reopen the temp file for writing explicitly
module main
import os
import io.util
fn main() {
println('=== io.util Module Demo ===')
println('\n--- 1. Creating a Temporary File ---')
// Create a temporary file. The '*' character in the pattern is replaced by a random number.
// tfo.path defaults to the system temp directory if not specified.
mut temp_file, temp_file_path := util.temp_file(pattern: 'v_guide_demo_*.txt') or {
println('Failed to create temp file: ${err}')
return
}
// Close the returned file handle immediately so we can open it with a clean write mode
temp_file.close()
defer {
os.rm(temp_file_path) or {}
println('Cleaned up temporary file: ${temp_file_path}')
}
println('Created temporary file at: ${temp_file_path}')
// Reopen the temp file for writing explicitly
mut f := os.open_file(temp_file_path, 'w') or {
println('Failed to open temp file for writing: ${err}')
return
}
// Write content into the temporary file
f.write('This is some temporary data written to a temp file.'.bytes()) or {
println('Failed to write to temp file: ${err}')
f.close()
return
}
f.close() // Close file to flush buffer and allow reading
// Read content to verify
content := os.read_file(temp_file_path) or {
println('Failed to read temp file: ${err}')
return
}
println('Temp file content: "${content}"')
println('\n--- 2. Creating a Temporary Directory ---')
// Create a temporary directory using a pattern
temp_dir_path := util.temp_dir(pattern: 'v_guide_dir_*') or {
println('Failed to create temp directory: ${err}')
return
}
// Register cleanup on function exit
defer {
os.rmdir_all(temp_dir_path) or {}
println('Cleaned up temporary directory: ${temp_dir_path}')
}
println('Created temporary directory at: ${temp_dir_path}')
// Create a sub-file inside the temporary directory
sub_file_path := os.join_path(temp_dir_path, 'sub_file.txt')
os.write_file(sub_file_path, 'Data saved inside temporary directory.') or {
println('Failed to create sub-file: ${err}')
return
}
println('Created sub-file: ${sub_file_path}')
// List files in the temporary directory to verify
dir_files := os.ls(temp_dir_path) or {
println('Failed to list temp directory: ${err}')
return
}
println('Temporary directory contents: ${dir_files}')
}Hash
In plain words: Computes fast non-cryptographic checksum numbers (crc32, fnv1a) used for hash tables and quick data integrity checks.
This example demonstrates calculating FNV-1a (32-bit and 64-bit) hashes and CRC32 checksums using the hash module.
What happens in this example:
- Code note: 1. FNV-1a 32-bit and 64-bit string hashing
- Code note: 2. CRC32 IEEE checksum
module main
import hash.fnv1a
import hash.crc32
fn main() {
println('=== hash Module Demo ===')
input := 'V language standard library'
println('Input string: "${input}"')
// 1. FNV-1a 32-bit and 64-bit string hashing
fnv_32 := fnv1a.sum32_string(input)
fnv_64 := fnv1a.sum64_string(input)
println('FNV-1a 32-bit hash: ${fnv_32}')
println('FNV-1a 64-bit hash: ${fnv_64}')
// 2. CRC32 IEEE checksum
crc_val := crc32.sum(input.bytes())
println('CRC32 checksum: ${crc_val}')
}Bitfield
In plain words: A BitField packs dozens of Yes/No (1/0) switches tightly into memory and lets you set, clear, toggle, or flip individual bits by position.
This example demonstrates creating bitfields, getting/setting individual bits, performing logical operations (AND, OR, XOR, NOT), and converting to string representation using the bitfield module.
What happens in this example:
- Code note: 1. Create bitfield from string
- Code note: 2. Accessing and modifying individual bits
- Code note: 3. Logical bitwise operations
module main
import bitfield
fn main() {
println('=== bitfield Module Demo ===')
// 1. Create bitfield from string
mut bf1 := bitfield.from_str('101100')
println('BitField 1 (from str): ${bf1.str()}')
println('Size of BitField 1: ${bf1.get_size()}')
println('Number of 1s (pop_count): ${bf1.pop_count()}')
// 2. Accessing and modifying individual bits
println('\nModifying individual bits:')
println(' Bit at index 1 before: ${bf1.get_bit(1)}')
bf1.set_bit(1)
println(' Bit at index 1 after set: ${bf1.get_bit(1)}')
bf1.clear_bit(0)
println(' Bitfield after changes: ${bf1.str()}')
// 3. Logical bitwise operations
mut bf2 := bitfield.from_str('011010')
println('\nLogical operations on ${bf1.str()} and ${bf2.str()}:')
and_result := bitfield.bf_and(bf1, bf2)
or_result := bitfield.bf_or(bf1, bf2)
xor_result := bitfield.bf_xor(bf1, bf2)
not_result := bitfield.bf_not(bf1)
println(' AND: ${and_result.str()}')
println(' OR: ${or_result.str()}')
println(' XOR: ${xor_result.str()}')
println(' NOT: ${not_result.str()}')
}Cli
In plain words: The cli module builds multi-command terminal tools (like git clone or docker run) with nested subcommands, flags, and auto-generated help screens.
This example demonstrates building structured CLI applications with commands, subcommands, and option flags in POSIX mode using the cli module.
What happens in this example:
- Code note: Test by parsing args mock
module main
import cli
fn main() {
println('=== cli Module Demo ===')
mut app := cli.Command{
name: 'tool'
description: "A sample CLI tool showing V's cli package."
version: '1.0.0'
posix_mode: true
execute: fn (cmd cli.Command) ! {
println('Root command execution. Use --help to see subcommands.')
}
commands: [
cli.Command{
name: 'greet'
description: 'Greet a user with custom options'
posix_mode: true
execute: fn (cmd cli.Command) ! {
name := cmd.flags.get_string('name') or { 'Guest' }
verbose := cmd.flags.get_bool('verbose') or { false }
if verbose {
println('Log: Initiating greeting process...')
}
println('Hello, ${name}!')
}
flags: [
cli.Flag{
flag: .string
name: 'name'
abbrev: 'n'
description: 'Name of person to greet'
},
cli.Flag{
flag: .bool
name: 'verbose'
abbrev: 'v'
description: 'Enable verbose logging'
},
]
},
]
}
app.setup()
// Test by parsing args mock
println('\nParsing args: tool greet --name Antigravity -v')
app.parse(['tool', 'greet', '--name', 'Antigravity', '-v'])
}Veb
In plain words: veb is V's fast built-in web server framework: define a Context struct and write normal V methods (@['/hello']) to serve web pages and JSON APIs!
This example demonstrates building a full-featured REST API with full CRUD operations (GET, POST, PUT, DELETE), clean helper functions for request parsing and validation (parse_and_validate_task), standardized JSON response functions (send_json, send_error, send_message), thread-safe state management (sync.RwMutex), and file-based JSON database persistence using the veb web framework.
module main
import json2
import net.http
import os
import sync
import time
import veb
// ============================================================================
// 1. DATA MODEL & VALIDATION
// ============================================================================
// Task represents a item in our system stored in a JSON file.
struct Task {
mut:
id int @[json: 'id']
title string @[json: 'title']
details string @[json: 'details']
completed bool @[json: 'completed']
}
// validate checks that incoming task data meets our business rules.
fn (t Task) validate() ! {
if t.title.trim_space() == '' {
return error('Task title cannot be empty')
}
}
// ============================================================================
// 2. DATABASE LAYER (JSON FILE PERSISTENCE)
// ============================================================================
struct Database {
mut:
file_path string
tasks []Task
}
fn (mut db Database) load() ! {
if !os.exists(db.file_path) {
db.tasks = []Task{}
return
}
content := os.read_file(db.file_path)!
if content.trim_space() == '' {
db.tasks = []Task{}
return
}
db.tasks = json2.decode[[]Task](content)!
}
fn (mut db Database) save() ! {
encoded := json2.encode(db.tasks, prettify: true)
os.write_file(db.file_path, encoded)!
}
// CRUD operations on the database
fn (db &Database) get_all() []Task {
return db.tasks
}
fn (db &Database) get_by_id(id int) ?Task {
for task in db.tasks {
if task.id == id {
return task
}
}
return none
}
fn (mut db Database) add(new_task Task) !Task {
new_task.validate()!
mut max_id := 0
for t in db.tasks {
if t.id > max_id {
max_id = t.id
}
}
created := Task{
id: max_id + 1
title: new_task.title.trim_space()
details: new_task.details.trim_space()
completed: new_task.completed
}
db.tasks << created
db.save()!
return created
}
fn (mut db Database) update(id int, update_data Task) !Task {
update_data.validate()!
for i in 0 .. db.tasks.len {
if db.tasks[i].id == id {
db.tasks[i].title = update_data.title.trim_space()
db.tasks[i].details = update_data.details.trim_space()
db.tasks[i].completed = update_data.completed
db.save()!
return db.tasks[i]
}
}
return error('Task with ID ${id} not found')
}
fn (mut db Database) delete(id int) ! {
for i, t in db.tasks {
if t.id == id {
db.tasks.delete(i)
db.save()!
return
}
}
return error('Task with ID ${id} not found')
}
// ============================================================================
// 3. HTTP APP & CONTEXT DEFINITION
// ============================================================================
struct App {
mut:
lock sync.RwMutex
db Database
}
pub struct Context {
veb.Context
}
// ============================================================================
// 4. REQUEST & RESPONSE HELPERS (ABSTRACTION LAYER)
// ============================================================================
// parse_and_validate_task parses JSON request body and validates field constraints.
fn parse_and_validate_task(mut ctx Context) !Task {
if ctx.req.data.trim_space() == '' {
return error('Request body cannot be empty')
}
task := json2.decode[Task](ctx.req.data) or {
return error('Invalid JSON payload structure')
}
task.validate()!
return task
}
// send_json encodes data as JSON and sets the HTTP status code.
fn send_json[T](mut ctx Context, data T, status http.Status) veb.Result {
ctx.res.set_status(status)
return ctx.json(json2.encode(data))
}
// send_error sets an HTTP error status code and returns a JSON error response.
fn send_error(mut ctx Context, message string, status http.Status) veb.Result {
ctx.res.set_status(status)
return ctx.json('{"error": "${message}"}')
}
// send_message sets an HTTP status code and returns a success JSON message.
fn send_message(mut ctx Context, message string, status http.Status) veb.Result {
ctx.res.set_status(status)
return ctx.json('{"message": "${message}"}')
}
// ============================================================================
// 5. ROUTE HANDLERS (CLEAN & CONCISE)
// ============================================================================
// GET / - Index welcome page
pub fn (app &App) index(mut ctx Context) veb.Result {
return ctx.text('Welcome to veb Clean CRUD API!')
}
// GET /api/tasks - Retrieve all tasks (READ)
@['/api/tasks'; get]
pub fn (mut app App) get_tasks(mut ctx Context) veb.Result {
app.lock.@rlock()
defer { app.lock.runlock() }
return send_json(mut ctx, app.db.get_all(), .ok)
}
// GET /api/tasks/:id - Retrieve a task by ID (READ)
@['/api/tasks/:id'; get]
pub fn (mut app App) get_task_by_id(mut ctx Context, id int) veb.Result {
app.lock.@rlock()
defer { app.lock.runlock() }
task := app.db.get_by_id(id) or {
return send_error(mut ctx, 'Task not found', .not_found)
}
return send_json(mut ctx, task, .ok)
}
// POST /api/tasks - Create a new task (CREATE)
@['/api/tasks'; post]
pub fn (mut app App) create_task(mut ctx Context) veb.Result {
payload := parse_and_validate_task(mut ctx) or {
return send_error(mut ctx, err.msg(), .bad_request)
}
app.lock.@lock()
defer { app.lock.unlock() }
created := app.db.add(payload) or {
return send_error(mut ctx, err.msg(), .internal_server_error)
}
return send_json(mut ctx, created, .created)
}
// PUT /api/tasks/:id - Update an existing task (UPDATE)
@['/api/tasks/:id'; put]
pub fn (mut app App) update_task(mut ctx Context, id int) veb.Result {
payload := parse_and_validate_task(mut ctx) or {
return send_error(mut ctx, err.msg(), .bad_request)
}
app.lock.@lock()
defer { app.lock.unlock() }
updated := app.db.update(id, payload) or {
status := if err.msg().contains('not found') { http.Status.not_found } else { http.Status.internal_server_error }
return send_error(mut ctx, err.msg(), status)
}
return send_json(mut ctx, updated, .ok)
}
// DELETE /api/tasks/:id - Delete a task by ID (DELETE)
@['/api/tasks/:id'; delete]
pub fn (mut app App) delete_task(mut ctx Context, id int) veb.Result {
app.lock.@lock()
defer { app.lock.unlock() }
app.db.delete(id) or {
return send_error(mut ctx, err.msg(), .not_found)
}
return send_message(mut ctx, 'Task deleted successfully', .ok)
}
// ============================================================================
// 6. MAIN DEMONSTRATION SUITE
// ============================================================================
fn main() {
println('=== veb Web Framework Full CRUD Demo (Clean Abstractions) ===')
db_file := 'veb_tasks_db.json'
defer {
if os.exists(db_file) {
os.rm(db_file) or {}
println('Cleaned up temporary database file: ${db_file}')
}
}
mut db := Database{ file_path: db_file }
db.load() or {}
mut app := &App{ db: db }
port := 30088
// Start server in background thread
spawn fn [mut app, port] () {
println('Starting veb server on http://localhost:${port}/...')
veb.run[App, Context](mut app, port)
}()
time.sleep(250 * time.millisecond)
base_url := 'http://localhost:${port}'
println('\n--- 1. POST /api/tasks (Validation Failure Test) ---')
invalid_json := '{"title": " ", "details": "No title provided"}'
resp_invalid := http.post_json('${base_url}/api/tasks', invalid_json) or { panic(err) }
println('Status: ${resp_invalid.status_code} | Body: ${resp_invalid.body}')
println('\n--- 2. POST /api/tasks (Create Task 1) ---')
valid_json1 := '{"title": "Learn V veb Abstractions", "details": "Clean helper functions for CRUD", "completed": false}'
resp_create1 := http.post_json('${base_url}/api/tasks', valid_json1) or { panic(err) }
println('Status: ${resp_create1.status_code} | Body: ${resp_create1.body}')
println('\n--- 3. POST /api/tasks (Create Task 2) ---')
valid_json2 := '{"title": "Build Clean REST API", "details": "Abstracted validation and responses", "completed": false}'
resp_create2 := http.post_json('${base_url}/api/tasks', valid_json2) or { panic(err) }
println('Status: ${resp_create2.status_code} | Body: ${resp_create2.body}')
println('\n--- 4. GET /api/tasks (List All Tasks) ---')
resp_all := http.get('${base_url}/api/tasks') or { panic(err) }
println('Status: ${resp_all.status_code} | Body:\n${resp_all.body}')
println('\n--- 5. GET /api/tasks/1 (Fetch Task by ID) ---')
resp_get := http.get('${base_url}/api/tasks/1') or { panic(err) }
println('Status: ${resp_get.status_code} | Body: ${resp_get.body}')
println('\n--- 6. PUT /api/tasks/1 (Update Task 1 to completed) ---')
update_json := '{"title": "Learn V veb Abstractions (Completed)", "details": "Clean helper functions for CRUD", "completed": true}'
resp_put := http.fetch(http.FetchConfig{
url: '${base_url}/api/tasks/1'
method: .put
header: http.new_header(http.HeaderConfig{ key: .content_type, value: 'application/json' })
data: update_json
}) or { panic(err) }
println('Status: ${resp_put.status_code} | Body: ${resp_put.body}')
println('\n--- 7. DELETE /api/tasks/2 (Delete Task 2) ---')
resp_del := http.fetch(http.FetchConfig{
url: '${base_url}/api/tasks/2'
method: .delete
}) or { panic(err) }
println('Status: ${resp_del.status_code} | Body: ${resp_del.body}')
println('\n--- 8. GET /api/tasks (Final Task List) ---')
resp_final := http.get('${base_url}/api/tasks') or { panic(err) }
println('Status: ${resp_final.status_code} | Body: ${resp_final.body}')
println('\n--- 9. Check JSON File Content on Disk ---')
if os.exists(db_file) {
db_content := os.read_file(db_file) or { '' }
println('JSON DB File (${db_file}) Content:\n${db_content}')
}
println('\nveb Full CRUD with clean abstractions completed successfully!')
}Readline
In plain words: The readline module reads user typing from the terminal with line-editing support (arrow keys, backspace, and history).
This example demonstrates prompting users for text input from terminal lines in a structured manner using the readline module.
What happens in this example:
- Code note: Read a line from standard input
module main
import readline
fn main() {
println('=== readline Module Demo ===')
mut r := readline.Readline{}
println('Simulating readline input (feed via stdin if non-interactive):')
// Read a line from standard input
line := r.read_line('Enter text: ') or {
println('Error or EOF: ${err}')
return
}
println('You entered: "${line}"')
}Runtime
In plain words: The runtime module tells your program how many CPU cores the computer has (nr_cpus()), how many processor threads are available, and whether it is 32-bit or 64-bit.
This example demonstrates inspecting hardware specifications, processor cores, system endianness, and memory usage statistics using the runtime module.
What happens in this example:
- Code note: 1. CPU and job info
- Code note: 2. System architecture details
- Code note: 3. Memory statistics
- Code note: Format to megabytes
module main
import runtime
fn main() {
println('=== runtime Module Demo ===')
// 1. CPU and job info
cpus := runtime.nr_cpus()
jobs := runtime.nr_jobs()
println('CPU Cores: ${cpus}')
println('Concurrent Jobs (VJOBS): ${jobs}')
// 2. System architecture details
println('Is 64-bit architecture? ${runtime.is_64bit()}')
println('Is 32-bit architecture? ${runtime.is_32bit()}')
println('Is Little Endian? ${runtime.is_little_endian()}')
println('Is Big Endian? ${runtime.is_big_endian()}')
// 3. Memory statistics
total_mem := runtime.total_memory() or { 0 }
free_mem := runtime.free_memory() or { 0 }
used_mem := runtime.used_memory() or { 0 }
// Format to megabytes
total_mb := total_mem / (1024 * 1024)
free_mb := free_mem / (1024 * 1024)
used_mb := used_mem / (1024 * 1024)
println('\nPhysical Memory info:')
println(' Total Memory: ${total_mb} MB')
println(' Free Memory: ${free_mb} MB')
println(' Used (by App): ${used_mb} MB')
}Strings.Lorem Helper
Strings Lorem
In plain words: Generates placeholder 'Lorem ipsum...' paragraphs and words of any length for testing layouts and templates.
V's standard library strings.lorem module provides a pseudo-random text generator based on a Markov chain built from embedded corpora. It produces structured text in the form of paragraphs and sentences, with options to control layouts, select specific corpora, and configure deterministic output.
This example demonstrates how to:
- Generate pseudo-random text with default configurations.
- Customize the output layout by adjusting words per sentence, sentences per paragraph, and paragraphs.
- Select specific corpora such as
lorem(Latin),poe(Edgar Allan Poe),darwin(Charles Darwin), andbard(William Shakespeare). - Run deterministic generation using an RNG seed and a custom starting phrase.
module main
import strings.lorem
fn main() {
println('=== V strings.lorem Standard Library Demo ===')
// 1. Basic Generation with Default Configuration
// By default, it will choose a random corpus, seed phrase, and RNG seed.
println('\n--- 1. Default Lorem Ipsum Generation ---')
default_lorem := lorem.generate(lorem.LoremCfg{})
println(default_lorem)
// 2. Custom Layout Configuration (Paragraphs, Sentences, Words)
println('\n--- 2. Custom Layout Generation ---')
custom_layout := lorem.generate(lorem.LoremCfg{
paragraphs: 2
sentences_per_paragraph: 3
words_per_sentence: 6
})
println(custom_layout)
// 3. Selection of Specific Embedded Corpora
// V's strings.lorem module supports four built-in corpora:
// - 'lorem' (Standard Latin Lorem Ipsum)
// - 'poe' (Edgar Allan Poe's The Raven)
// - 'darwin' (Charles Darwin's Origin of Species)
// - 'bard' (William Shakespeare's works)
println('\n--- 3. Specific Corpora Examples ---')
corpora := ['lorem', 'poe', 'darwin', 'bard']
for corpus in corpora {
text := lorem.generate(lorem.LoremCfg{
corpus_name: corpus
paragraphs: 1
sentences_per_paragraph: 2
words_per_sentence: 8
})
println('Corpus [${corpus}]:')
println(text)
println('-'.repeat(40))
}
// 4. Deterministic Text Generation using RNG Seed and Custom Seed Phrases
// Using a specific `rng_seed` guarantees that the generated pseudo-random text is deterministic
// and identical across multiple runs. Custom `seed_text` provides a starting phrase for the Markov chain.
println('\n--- 4. Deterministic Generation with Seed & Custom Starting Phrase ---')
deterministic_lorem_1 := lorem.generate(lorem.LoremCfg{
corpus_name: 'poe'
rng_seed: 42
seed_text: 'once upon a midnight'
paragraphs: 1
sentences_per_paragraph: 2
words_per_sentence: 8
})
deterministic_lorem_2 := lorem.generate(lorem.LoremCfg{
corpus_name: 'poe'
rng_seed: 42
seed_text: 'once upon a midnight'
paragraphs: 1
sentences_per_paragraph: 2
words_per_sentence: 8
})
println('Run 1:')
println(deterministic_lorem_1)
println('\nRun 2:')
println(deterministic_lorem_2)
// Assert that they are exactly identical due to deterministic seeding
assert deterministic_lorem_1 == deterministic_lorem_2
println('\nDeterministic assertion passed! Both runs generated identical text.')
}WebAssembly Compilation
V has first-class support for WebAssembly (WASM). There are two distinct methods for compiling and working with WebAssembly in V:
- Compiling V source code to WASM (using direct/native backends or Emscripten).
- Programmatic WASM generation (using the built-in
wasminstruction-level builder).
Compiling V Source to WebAssembly
1. Native Direct Backend (`-b wasm`)
V contains a native compiler backend that bypasses C intermediate code and outputs WebAssembly binary files (.wasm) directly. This backend has zero dependencies and is extremely fast, though it is currently in active development and supports a subset of the language.
To compile a V file directly to WASM:
v -b wasm -o main.wasm main.vExecuting in JavaScript/Node.js:
To run the natively compiled WASM binary, instantiate it using the standard JavaScript WebAssembly API:
const fs = require("fs");
async function run() {
const wasmBuffer = fs.readFileSync("main.wasm");
const { instance } = await WebAssembly.instantiate(wasmBuffer, {
env: {
// Import host functions here if needed
},
});
// Call exported V functions from JS
console.log(instance.exports.add(5, 10));
}
run();2. Emscripten C Backend (`-os wasm`)
For compiling complex applications, standard library features, or C-linked dependencies to WASM, V relies on the Emscripten toolchain. V translates the V code to intermediate C, and Emscripten compiles it to highly optimized WebAssembly.
Prerequisites:
Ensure the Emscripten SDK (emsdk) is installed and active on your PATH:
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
./emsdk install latest
./emsdk activate latest
source ./emsdk_env.shCompiling with V:
v -os wasm -o main.js main.vThis produces:
main.wasm: The compiled WebAssembly binary.main.js: Emscripten glue code to load the WASM module and map runtime environments (I/O, memory, filesystem).
Running in Node.js:
node main.jsProgrammatic WASM Generation
Programmatic WASM Builder
In plain words: Uses V's wasm module to generate a real .wasm WebAssembly binary file from V code instructions that can run inside Node.js or a web browser.
V provides a built-in wasm module in its standard library that allows developers to programmatically build WebAssembly binary (.wasm) files directly using instruction-level builder patterns. This is extremely useful for compilers, runtime engines, or dynamic code generation targeting the browser and other WebAssembly runtimes.
This example demonstrates how to:
- Initialize a WebAssembly module (
wasm.Module). - Generate exported arithmetic functions (
add,sub,mul). - Construct and read mutable global variables (
new_global,global_get,global_set). - Build recursive control structures (a factorial
facfunction utilizingc_if,c_else,c_end, and recursivecall). - Compile the module down to a
.wasmbinary slice ([]u8) and save it to disk.
module main
import wasm
import os
fn main() {
println('=== V WebAssembly (wasm) Module Demo ===')
// Initialize the WebAssembly module
mut m := wasm.Module{}
m.enable_debug('vlang_wasm_demo')
// --- 1. Basic Arithmetic Functions ---
println('\n1. Generating Arithmetic Functions (add, sub, mul)...')
// Exported 'add' function: taking two i32 parameters, returning one i32 result
mut add_fn := m.new_function('add', [.i32_t, .i32_t], [.i32_t])
{
add_fn.local_get(0)
add_fn.local_get(1)
add_fn.add(.i32_t)
}
m.commit(add_fn, true) // commit with export = true
// Exported 'sub' function
mut sub_fn := m.new_function('sub', [.i32_t, .i32_t], [.i32_t])
{
sub_fn.local_get(0)
sub_fn.local_get(1)
sub_fn.sub(.i32_t)
}
m.commit(sub_fn, true)
// Exported 'mul' function
mut mul_fn := m.new_function('mul', [.i32_t, .i32_t], [.i32_t])
{
mul_fn.local_get(0)
mul_fn.local_get(1)
mul_fn.mul(.i32_t)
}
m.commit(mul_fn, true)
// --- 2. Global Variables ---
println('\n2. Creating Global Variables...')
// Global variable named '__vsp' (Stack Pointer), internal/non-exported, type i32, mutable, init value 10
vsp := m.new_global('__vsp', false, .i32_t, true, wasm.constexpr_value(10))
// Create a function that retrieves the global value, adds 20, stores it back, and returns the new value
mut vsp_fn := m.new_function('update_vsp', [], [.i32_t])
{
vsp_fn.global_get(vsp)
vsp_fn.i32_const(20)
vsp_fn.add(.i32_t)
vsp_fn.global_set(vsp)
vsp_fn.global_get(vsp)
}
m.commit(vsp_fn, true)
// --- 3. Recursive Functions (Factorial) ---
println('\n3. Generating Recursive Function (fac)...')
// fac(n) returns n! using i64 types
mut fac_fn := m.new_function('fac', [.i64_t], [.i64_t])
{
fac_fn.local_get(0)
fac_fn.eqz(.i64_t)
// If block: if n == 0, return 1
ifs := fac_fn.c_if([], [.i64_t])
{
fac_fn.i64_const(1)
}
fac_fn.c_else(ifs)
{
// Else: return n * fac(n - 1)
fac_fn.local_get(0) // push n
fac_fn.local_get(0)
fac_fn.i64_const(1)
fac_fn.sub(.i64_t) // n - 1
fac_fn.call('fac') // recursive call to fac(n - 1)
fac_fn.mul(.i64_t) // n * fac(n - 1)
}
fac_fn.c_end(ifs)
}
m.commit(fac_fn, true)
// --- 4. Compilation & Output ---
println('\n4. Compiling Module to WebAssembly Binary...')
binary_code := m.compile()
println('Compilation Successful! Binary size: ${binary_code.len} bytes')
// Save compiled binary as output.wasm in the current directory
dir := os.dir(@FILE)
output_path := os.join_path(dir, 'output.wasm')
os.write_file(output_path, binary_code.bytestr()) or {
println('Failed to write output.wasm: ${err}')
return
}
println('Saved Wasm binary to: ${output_path}')
}sizeof and \_\_offsetof
In plain words: sizeof(Type) tells you how many bytes a type takes in memory, and __offsetof(Struct, field) tells you the exact byte offset where a field starts.
V provides two built-in operators for determining sizes and memory offsets:
sizeof(Type): Returns the memory size of the given Type in bytes.__offsetof(Struct, field_name): Returns the offset in bytes of a field relative to the start of the struct.
Step-by-Step Code Walkthrough:
- Size of
Point:
sizeof(Point) yields 8 bytes because it has two int fields (4 bytes each).
- Size of
Foo& Alignment Padding:
sizeof(Foo) yields 12 bytes, even though it contains a int (4 bytes), b u8 (1 byte), and c int (4 bytes). This happens because of C ABI alignment: V aligns struct members on word boundaries (in this case, 4 bytes), adding 3 bytes of invisible padding after b u8.
- Offsets of fields in
Foo: __offsetof(Foo, a)yields0because it starts at byte 0.__offsetof(Foo, b)yields4because it starts right aftera(4 bytes).__offsetof(Foo, c)yields8because it aligns on the next 4-byte boundary due to padding afterb.
module main
struct Point {
x int
y int
}
struct Foo {
a int
b u8
c int
}
fn main() {
// sizeof gives the size of a type in bytes
println('sizeof(Point) = ${sizeof(Point)} bytes') // 8
println('sizeof(Foo) = ${sizeof(Foo)} bytes') // 12 (due to alignment/padding in C backend)
// __offsetof gives the offset in bytes of a struct field
println('__offsetof(Point, x) = ${__offsetof(Point, x)}') // 0
println('__offsetof(Point, y) = ${__offsetof(Point, y)}') // 4
println('__offsetof(Foo, a) = ${__offsetof(Foo, a)}') // 0
println('__offsetof(Foo, b) = ${__offsetof(Foo, b)}') // 4
println('__offsetof(Foo, c) = ${__offsetof(Foo, c)}') // 8
}Limited Operator Overloading
In plain words: Lets you define what math symbols (+, -, *, /, <, ==) mean for your own custom structs (such as adding two 2D Vec2 vectors with v1 + v2).
Operator overloading is supported for a limited set of binary operators (+, -, *, **, /, %, <, ==) to improve readability in scientific, mathematical, and graphics applications. V does not support indexing ([]) or assignment (=) overloading. Overloading the + operator automatically synthesizes the matching assignment operator (e.g., +=).
Step-by-Step Code Walkthrough:
- Binary Operator Overloading:
We define the + and - operators for struct Vec using the syntax fn (a Vec) + (b Vec) Vec. V invokes these methods directly when performing arithmetic expressions like a + b or a - b.
- Equality Operator Overloading:
We define == to verify element-by-element equality: a.x == b.x && a.y == b.y.
- Automatic Assignment Generation:
When we overload +, V autogenerates the compound assignment operator +=. Executing c += a resolves to c = c + a, compiling and modifying c automatically.
module main
struct Vec {
x int
y int
}
fn (a Vec) str() string {
return '{${a.x}, ${a.y}}'
}
// Overload addition operator (+)
fn (a Vec) + (b Vec) Vec {
return Vec{a.x + b.x, a.y + b.y}
}
// Overload subtraction operator (-)
fn (a Vec) - (b Vec) Vec {
return Vec{a.x - b.x, a.y - b.y}
}
// Overload equality operator (==)
fn (a Vec) == (b Vec) bool {
return a.x == b.x && a.y == b.y
}
fn main() {
a := Vec{2, 3}
b := Vec{4, 5}
mut c := Vec{1, 2}
println('a + b = ${a + b}') // {6, 8}
println('a - b = ${a - b}') // {-2, -2}
// c += a is autogenerated from the + overload!
c += a
println('c after += a: ${c}') // {3, 5}
println('a == b: ${a == b}') // false
println('a == Vec{2, 3}: ${a == Vec{2, 3}}') // true
}Atomics
In plain words: Atomic operations update a number in memory in a single uninterruptible hardware step so multiple CPU threads can increment counters safely.
V does not have direct keyword support for atomic operations but integrates standard C11 atomic capabilities through platform-specific wrapper headers (found under stdatomic/). Variables can be treated atomically by executing type-specific functions prefixed by C.atomic_ and passing references to the target variables.
Step-by-Step Code Walkthrough:
- C Header Inclusion:
Depending on the compilation target, V includes either the Unix or Windows wrapper headers atomic.h containing atomic operations.
- C declarations:
fn C.atomic_store_u32(&u32, u32) and other methods are declared. Since V does not automatically parse C headers, we manually declare the C functions we want to invoke.
- Local Storage:
We declare a local variable atom := u32(0) inside main(). Any variable can be treated atomically by passing its memory reference (pointer) to the C functions. This avoids needing global variables or compiling with -enable-globals flag.
- CAS (Compare-And-Swap):
Inside unsafe { ... }, we call C.atomic_compare_exchange_strong_u32(&atom, &expected, 23). If the value at &atom matches expected (17), it replaces it with 23 and returns true. If not, it loads the actual current value into expected and returns false.
module main
$if windows {
#include "@VEXEROOT/thirdparty/stdatomic/win/atomic.h"
} $else {
#include "@VEXEROOT/thirdparty/stdatomic/nix/atomic.h"
}
// declare the C functions we want to use
fn C.atomic_store_u32(&u32, u32)
fn C.atomic_load_u32(&u32) u32
fn C.atomic_compare_exchange_strong_u32(&u32, &u32, u32) bool
fn main() {
// Ordinary local variable, treated as atomic by passing its reference
mut atom := u32(0)
// Initialize atomic variable
unsafe {
C.atomic_store_u32(&atom, 17)
mut expected := u32(17)
// Atomic CAS: if atom == expected, set atom to 23 and return true
if C.atomic_compare_exchange_strong_u32(&atom, &expected, 23) {
println('Exchange successful, atom is now 23')
} else {
println('Exchange failed, atom is ${C.atomic_load_u32(&atom)}')
}
println('Final value: ${C.atomic_load_u32(&atom)}')
}
}Static Variables
In plain words: In -translated low-level code, a static variable inside a function keeps its value between calls instead of resetting each time.
V supports Static Variables within functions. They behave like namespaced global variables, preserving state across function calls, but are restricted to the local scope of a single function. Functions containing static variables must be marked with @[unsafe] and calls must occur in unsafe blocks. They are primarily intended for facilitating low-level C code translations.
Step-by-Step Code Walkthrough:
- Static Variable Definition:
Inside the function counter(), we define mut static x := 42. The compiler generates this variable in the global data space but restricts access to it exclusively within counter().
- One-Time Initialization:
The initialization expression = 42 is executed exactly once when the program starts. It is skipped on subsequent calls to counter().
- Access Rules:
Because static variables represent shared mutable state, counter() is marked with @[unsafe] and called within unsafe blocks in main().
module main
// V supports function-scoped static variables inside unsafe functions
@[unsafe]
fn counter() int {
// static variables are initialized only once
mut static x := 42
x++
return x
}
fn main() {
println(unsafe { counter() }) // 43
println(unsafe { counter() }) // 44
println(unsafe { counter() }) // 45
}Hot Code Reloading
In plain words: Tagging a function with @[live] and running with v -live lets you edit your code and see changes live on screen without restarting your app!
V supports Hot Code Reloading using the @[live] attribute. By compiling a program with the -live flag (e.g. v -live run file.v), V monitors the source files, automatically recompiles live-annotated functions to a shared library, and reloads them dynamically at runtime without restarting the application.
Step-by-Step Code Walkthrough:
- Live Annotation:
The function print_message() is marked with the @[live] attribute. This instructs the compiler to generate it as a hot-reloadable hook loading from a shared library.
- Main loop execution:
The loop in main() runs continuously. If you modify the message in the string print inside print_message() and save the file, V's monitoring thread detects the change, rebuilds the code, and subsequent calls in the loop print the updated message instantly.
module main
import time
// Functions that should be reloaded must have `@[live]` attribute
@[live]
fn print_message() {
println('Hello! Modify this message while the program is running under -live mode.')
}
fn main() {
// A simple loop printing the message
for i in 0 .. 3 {
print_message()
time.sleep(100 * time.millisecond)
}
}Compile-Time Reflection
In plain words: Using $for field in Struct.fields lets you loop over a struct's fields at compile time to build your own automatic printers, validators, or serializers.
V supports Compile-Time Reflection using the $ prefix to perform type operations, evaluations, and code generation during compilation. V iterates over struct fields, attributes, variants, and methods at compile-time using $for loops, providing static safety without any runtime metadata overhead.
- Struct fields can be examined via
Struct.fields. - Struct attributes can be examined via
Struct.attributes. - Struct methods can be examined via
Struct.methods, and executed dynamically using$method().
Step-by-Step Code Walkthrough:
- Field Iteration (
$for field in User.fields):
Iterates over all fields of User. Evaluates their names (field.name) and type IDs (field.typ) at compile-time.
- Attribute Iteration (
$for attr in User.attributes):
Inspects attributes attached to the struct, such as @[COLOR] (prints COLOR).
- Comptime Method Invocation (
user.$method()):
Inside $for m in User.methods, we check if the method returns a string. If so, we execute the method dynamically using the $method() comptime syntax, calling greet() on the user instance and printing Hello Alice.
module main
@[COLOR]
struct User {
name string
age int
}
fn (u User) greet() string {
return 'Hello ${u.name}'
}
fn main() {
println('--- Struct Fields Reflection ---')
$for field in User.fields {
println('Field: ${field.name} | Typ: ${field.typ}')
}
println('\n--- Struct Attributes Reflection ---')
$for attr in User.attributes {
println('Attribute name: ${attr.name}')
}
println('\n--- Struct Methods Reflection ---')
user := User{
name: 'Alice'
age: 30
}
$for m in User.methods {
$if m.return_type is string {
println(user.$method())
}
}
}Compile-Time Pseudo Variables
In plain words: Special @ words like @FILE (current file name), @LINE (current line number), and @FN (current function name) are filled in automatically by the compilerโgreat for debugging logs!
V provides a set of pseudo-variables starting with @ that are evaluated and substituted at compile time:
- Scope Identifiers:
@FN-> Replaced with the name of the current V function (as a string).@METHOD-> Replaced with theReceiverType.MethodNameof the current method (as a string).@MOD-> Replaced with the name of the current V module (as a string).@STRUCT-> Replaced with the name of the current V struct (as a string).
- Source File & Location Identifiers:
@FILE-> Replaced with the absolute path of the V source file (as a string).@DIR-> Replaced with the absolute path of the folder containing the V source file (as a string).@LINE-> Replaced with the V line number where it appears (as a string).@FILE_LINE-> Like@FILE:@LINE, but the file part is a relative path (as a string).@LOCATION-> Combines file, line, and type/method name; suitable for logging.@COLUMN-> Replaced with the 1-based column offset where it appears (as a string).
- V Compiler & Git Identifiers:
@VEXE-> Replaced with the path to the V compiler executable (as a string).@VEXEROOT-> Replaced with the folder containing the V compiler executable (as a string).@VHASH-> Replaced with the shortened commit hash of the V compiler (as a string).@VCURRENTHASH-> Similar to@VHASH, but updates when the compiler is recompiled after local modifications or git bisect.
- Project Mod Info (requires
v.modin project root): @VMOD_FILE-> Replaced with the contents of the nearestv.modfile (as a string).@VMODHASH-> Replaced with the shortened commit hash derived from the.gitdirectory next to the nearestv.modfile (as a string).@VMODROOT-> Replaced with the path to the directory containing the nearestv.modfile (as a string).
- Build Time Identifiers (UTC timezone):
@BUILD_DATE-> Replaced with the build date (e.g.'2026-06-26').@BUILD_TIME-> Replaced with the UTC build time (e.g.'17:50:24').@BUILD_TIMESTAMP-> Replaced with the Unix timestamp of the build (e.g.'1782496224').- Note: Build variables can be overridden by setting the
SOURCE_DATE_EPOCHenvironment variable, enabling reproducible builds (e.g., setting it to the latest git commit timestamp).
- Target Platform / Toolchain Identifiers:
@OS-> Replaced with the OS type (e.g.'macos','linux','windows').@CCOMPILER-> Replaced with the C compiler used (e.g.'gcc','clang').@BACKEND-> Replaced with the current language backend (e.g.'c','js').@PLATFORM-> Replaced with the CPU architecture type (e.g.'arm64','amd64').
This example demonstrates the usage and output of all the available compile-time pseudo variables in V.
module main
struct User {
name string
}
fn (u User) register() {
println('Executing method: ' + @METHOD) // User.register
println('Defined in struct: ' + @STRUCT) // User
}
fn log_event() {
println('Logging from function: ' + @FN) // log_event
}
fn main() {
println('=== V Compile-Time Pseudo Variables ===')
// Module & File Info
println('Current Module: ' + @MOD)
println('Source File Path: ' + @FILE)
println('Source Directory: ' + @DIR)
println('Line Number: ' + @LINE.str())
println('Relative File/Line: ' + @FILE_LINE)
println('Log Location: ' + @LOCATION)
println('Column Number: ' + @COLUMN.str())
// Compiler Info
println('V Compiler Executable: ' + @VEXE)
println('V compiler Root Directory: ' + @VEXEROOT)
println('V Compiler Commit Hash: ' + @VHASH)
println('V Compiler Current Hash: ' + @VCURRENTHASH)
// project Info (from v.mod)
println('v.mod File Contents: ' + @VMOD_FILE)
println('v.mod Git Commit Hash: ' + @VMODHASH)
println('v.mod Root Directory: ' + @VMODROOT)
// Build Info (UTC timezone)
println('Build Date: ' + @BUILD_DATE)
println('Build Time: ' + @BUILD_TIME)
println('Build Timestamp: ' + @BUILD_TIMESTAMP)
// Platform / Backend Info
println('Target OS: ' + @OS)
println('C Compiler: ' + @CCOMPILER)
println('V Backend: ' + @BACKEND)
println('CPU Platform: ' + @PLATFORM)
u := User{
name: 'Alice'
}
u.register()
log_event()
}Environment-Specific Files & Compile-Time Types
Environment-Specific Files & Compile-Time Types
Environment-Specific Files
V supports compile-time filtering of entire files using file suffix conventions instead of conditional directives:
.js.v-> compiled only when targeting the Javascript backend..c.v-> compiled only when targeting the C backend._nix.c.v-> compiled only on Unix-like platforms._windows.c.v-> compiled only on Windows._d_customflag.v-> compiled only if-d customflagis passed to the compiler.
Compile-Time Types
When writing generic code, V provides specialized compile-time type matching identifiers to selectively check generic constraints:
$alias-> matches type aliases.$array-> matches arrays and fixed-size arrays.$enum-> matches enum types.$float/$int/$string-> matches floating numbers, integers, or string values.$struct-> matches struct types.
References & Pointers
In plain words: Explains & references (pointing to an existing struct in memory instead of copying it) and how V keeps references safe without manual pointer arithmetic.
In V, references are similar to pointers in Go/C and references in C++. They allow you to point to a memory location of another variable without making a copy of its contents.
1. Passing by Value vs. Passing by Reference
When passing immutable arguments (like structs) to functions or methods, V decides under the hood whether to pass them by value or by reference depending on performance characteristics. As a developer, you do not need to worry about this optimization detail.
However, you can explicitly force an argument or method receiver to be passed by reference by prefixing the type with & (e.g., &Foo).
2. Mutability of References
References in V are immutable by default:
- Even if a function or method receives a reference (
&Foo), it cannot modify the fields of that struct unless the parameter is marked as mutable. - To allow modification, the argument must be declared as
mut foo Foo(which V automatically passes by reference under the hood) and called withmut(e.g.modify_foo(mut my_foo)). Note that modifiable fields must also be defined under themut:access block in the struct declaration.
3. Dereferencing
To access the underlying value of a reference directly, or to create a copy of the pointed-to object, use the dereferencing operator * (e.g., copied_foo := *ref_to_foo), similar to Go and C.
4. Recursive Structures
Recursive data structures (such as linked lists or trees) require fields that reference their own type. Because V needs to calculate the memory size of structs at compile time, recursive fields must be declared as references (e.g., left ?&Node[T]) because references have a fixed pointer size.
- To allow optional/empty references (like leaf node terminations in trees), V supports optional references (e.g.
?&Node[T]). These are automatically initialized tononeby default, avoiding the need forunsafe { nil }pointers or dummy nodes.
Here is the complete demonstration program showcasing V references and recursive struct designs:
module main
struct Foo {
mut:
abc int
}
// 1. A method receiving a reference. The receiver type is &Foo.
// Even though it is a reference, `foo` is immutable and cannot be changed here.
fn (foo &Foo) print_abc() {
println('print_abc: foo.abc = ${foo.abc}')
}
// 2. A regular function receiving a reference to Foo.
fn show_foo(foo &Foo) {
println('show_foo: foo.abc = ${foo.abc}')
}
// 3. To modify a reference, we must pass it as mutable.
// Note that mutable parameters are passed by reference under the hood.
fn modify_foo(mut foo Foo, new_val int) {
foo.abc = new_val
}
// 4. References are crucial for recursive types (like trees or linked lists).
// Since the size of Node must be known at compile time, recursive fields must be references.
// To allow optional/empty references (like leaf node terminations), V uses optional references (?&Node[T]).
struct Node[T] {
val T
left ?&Node[T]
right ?&Node[T]
}
fn main() {
println('=== V References & Pointers Demo ===')
// Creating a struct instance
mut my_foo := Foo{
abc: 100
}
// Calling method on reference. V automatically takes the address of my_foo.
my_foo.print_abc()
// Calling a function expecting a reference using & operator.
show_foo(&my_foo)
// Modifying the struct via a mutable receiver/argument.
modify_foo(mut my_foo, 200)
println('After modify_foo: my_foo.abc = ${my_foo.abc}')
// 5. Dereferencing a reference using the `*` operator.
ref_to_foo := &my_foo
// To copy the value of the struct pointed to by ref_to_foo:
copied_foo := *ref_to_foo
println('Copied foo abc: ${copied_foo.abc}')
// 6. Generic Tree structure using optional references
// Optional references are auto-initialized to `none`, so we don't need dummy nodes or `unsafe` blocks.
left_leaf := Node[int]{
val: 5
}
right_leaf := Node[int]{
val: 15
}
// Create root node pointing to leaf references
root := Node[int]{
val: 10
left: &left_leaf
right: &right_leaf
}
println('Root val: ${root.val}')
// Access the optional child nodes safely using if guards
if left := root.left {
println('Left leaf val: ${left.val}')
}
if right := root.right {
println('Right leaf val: ${right.val}')
}
}Dumping Expressions at Runtime
In plain words: Wrapping any expression in dump(x) prints [file.v:line] x: value to the terminal AND still returns xโthe fastest way to debug any calculation!
You can dump/trace the value of any V expression using dump(expr). For example, save this code sample as factorial.v, then run it with v run factorial.v:
What happens in this example:
- Creates helper function(s)/method(s):
factorial. - Prints the result to the terminal window so you can see the output right away.
module main
fn factorial(n u32) u32 {
if dump(n <= 1) {
return dump(1)
}
return dump(n * factorial(n - 1))
}
fn main() {
println(factorial(5))
}You will get:
[factorial.v:4] n <= 1: false
[factorial.v:4] n <= 1: false
[factorial.v:4] n <= 1: false
[factorial.v:4] n <= 1: false
[factorial.v:4] n <= 1: true
[factorial.v:5] 1: 1
[factorial.v:7] n * factorial(n - 1): 2
[factorial.v:7] n * factorial(n - 1): 6
[factorial.v:7] n * factorial(n - 1): 24
[factorial.v:7] n * factorial(n - 1): 120
120Chapter 14 Useful Boilerplates and Application Templates
Plain-English Chapter Guide (Start Here if You Are New to Coding!)
- The Big Idea in Simple Words: A boilerplate or template is a ready-made starter project! Instead of staring at a blank page when you want to build a command-line tool, a web server, a background worker pool, a desktop GUI app, or a 2D game, you can copy one of these complete, working starter templates and customize it for your own idea.
Quick Access โ Jump to any lesson in this chapter
Below is an index of all code examples in this chapter. You can use these links to jump directly to any specific code example:
Boilerplate Templates
- CLI Command-Line Application Boilerplate
- REST API Server Boilerplate
- Worker Pool Concurrency Boilerplate
- OS and File Utilities Boilerplate
- String Utilities Boilerplate
- Math and Statistics Boilerplate
- Array Utilities Boilerplate
- Configuration Management Boilerplate
- JSON File Store Boilerplate
- Retry and Backoff Boilerplate
- HTTP Client Boilerplate
- CSV Processor Boilerplate
- macOS Native GUI Boilerplate
- Logging Boilerplate
This chapter provides a collection of production-ready, fully commented boilerplate templates designed to jumpstart your development with V. These examples showcase common application patterns, standard library usage, and best practices.
Boilerplate Templates
CLI Command-Line Application Boilerplate
In plain words: A complete starter template for building a command-line tool with --input, --output, and --verbose flags using V's flag module.
When building command-line utilities, V provides a highly featured standard flag module. Rather than manually parsing arguments from os.args, the flag module simplifies declaring options, validation, and auto-generates helpful usage/help text.
Key concepts illustrated:
- Initializing flag parser: Creating a new parser with
flag.new_flag_parser(os.args). - Defining Flags: Specifying flag names, short character abbreviations, default values, and description text.
- Config Struct Pattern: Cleanly separating parsed options from application logic using a custom config struct.
- Error Handling & Finalization: Handling invalid command usage gracefully using
fp.finalize().
module main
import flag
import os
// Config holds the validated configuration settings for the application.
struct Config {
input_file string
output_file string
verbose bool
retries int
mode string
}
fn main() {
// 1. Initialize the flag parser with command-line arguments (os.args)
mut fp := flag.new_flag_parser(os.args)
fp.application('v-cli-boilerplate')
fp.version('1.0.0')
fp.description('A professional CLI boilerplate showing flag parsing, validation, and structured configurations in V.')
// 2. Skip the executable path during parsing
fp.skip_executable()
// 3. Define flags with short abbreviations, default values, and descriptions
input_file := fp.string('input', `i`, '', 'Path to the input file (required)')
output_file := fp.string('output', `o`, 'output.txt', 'Path to the output file')
verbose := fp.bool('verbose', `v`, false, 'Enable verbose logging')
retries := fp.int('retries', `r`, 3, 'Number of retries for operation')
mode := fp.string('mode', `m`, 'default', 'Operation mode (default, fast, safe)')
// 4. Finalize parsing. This returns remaining non-flag arguments or an error.
additional_args := fp.finalize() or {
eprintln('Error: ${err}')
println(fp.usage())
exit(1)
}
// 5. Validate required flags and values
if input_file == '' {
eprintln('Error: --input (-i) flag is required.')
println(fp.usage())
exit(1)
}
// Validate allowed values for a string enum
if mode !in ['default', 'fast', 'safe'] {
eprintln('Error: Invalid mode "${mode}". Must be one of: default, fast, safe.')
println(fp.usage())
exit(1)
}
// 6. Map parsed arguments to the Config struct for clean division of concerns
config := Config{
input_file: input_file
output_file: output_file
verbose: verbose
retries: retries
mode: mode
}
// 7. Run the application logic
run_app(config, additional_args)
}
fn run_app(cfg Config, args []string) {
if cfg.verbose {
println('[DEBUG] Starting application execution...')
println('[DEBUG] Config: ${cfg}')
if args.len > 0 {
println('[DEBUG] Positional Arguments: ${args}')
}
}
println('Processing input file: ${cfg.input_file}')
println('Operation mode: ${cfg.mode}')
println('Retries configured: ${cfg.retries}')
// Perform work here...
println('Writing results to output file: ${cfg.output_file}')
println('Success: Application executed successfully!')
}REST API Server Boilerplate
In plain words: A ready-to-run HTTP JSON web server template with routes for listing, creating, and looking up items.
V's standard web framework, veb, is optimized for building fast, high-conformance web apps and APIs. This template serves as a quick-start scaffolding for a JSON REST service, illustrating CRUD routing and request decoding.
Key concepts illustrated:
- Routing Attributes: Tagging methods with route paths and HTTP verbs (e.g.
@['/api/items'; get]). - Path Parameters: Defining routes with dynamic segments like
@['/api/items/:id'; get]which map directly to method arguments. - JSON Serialization/Deserialization: Using
json2.encodeandjson2.decodeto work with HTTP requests and responses. - State Management & Thread Safety: Using fields in the global
Appstruct to share resources, protected by async.RwMutexto ensure thread-safe concurrent access.
module main
import json2
import os
import sync
import veb
// Item represents a data model in our API.
struct Item {
id int @[json: 'id']
name string @[json: 'name']
done bool @[json: 'done']
}
// App holds the global state of the application.
struct App {
mut:
lock sync.RwMutex
items []Item
}
// Context wraps veb's request/response lifecycle.
struct Context {
veb.Context
}
// 1. GET / - Simple text response index endpoint
fn (mut app App) index(mut ctx Context) veb.Result {
return ctx.text('Welcome to the V REST API Boilerplate! Use /api/items to interact with the service.')
}
// 2. GET /api/items - Returns list of all items as JSON
@['/api/items'; get]
fn (mut app App) get_items(mut ctx Context) veb.Result {
app.lock.@rlock()
defer { app.lock.runlock() }
return ctx.json(json2.encode(app.items))
}
// 3. GET /api/items/:id - Returns a single item by id, or 404
@['/api/items/:id'; get]
fn (mut app App) get_item(mut ctx Context, id int) veb.Result {
app.lock.@rlock()
defer { app.lock.runlock() }
for item in app.items {
if item.id == id {
return ctx.json(json2.encode(item))
}
}
ctx.res.set_status(.not_found)
return ctx.json('{"error": "Item not found"}')
}
// 4. POST /api/items - Decodes JSON request body and adds a new item
@['/api/items'; post]
fn (mut app App) create_item(mut ctx Context) veb.Result {
new_item := json2.decode[Item](ctx.req.data) or {
ctx.res.set_status(.bad_request)
return ctx.json('{"error": "Invalid JSON format"}')
}
app.lock.@lock()
defer { app.lock.unlock() }
// Auto-increment ID based on length
item_to_add := Item{
id: app.items.len + 1
name: new_item.name
done: new_item.done
}
app.items << item_to_add
ctx.res.set_status(.created)
return ctx.json(json2.encode(item_to_add))
}
fn main() {
// Initialize App with mock seed data
mut app := &App{
items: [
Item{
id: 1
name: 'Learn V syntax'
done: true
},
Item{
id: 2
name: 'Build a REST API in V'
done: false
},
]
}
// Read port from environment variable or default to 8082 to avoid common conflicts on 8080
port_env := os.getenv('PORT')
port := if port_env != '' { port_env.int() } else { 8082 }
println('Starting REST API server on http://localhost:${port}...')
// Start veb web server
veb.run[App, Context](mut app, port)
}Worker Pool Concurrency Boilerplate
In plain words: A reusable concurrency template where a fixed team of background worker threads pulls jobs from a shared channel queue and sends back results.
V offers lightweight concurrency out of the box. By combining spawn (thread creation) with typed channels (chan), you can build thread-safe worker pools that process computationally intensive or high-latency tasks in parallel without lock overhead.
Key concepts illustrated:
- Channel Communication: Sending and receiving tasks and results over thread-safe queues.
- Spawned Threads: Running worker functions concurrently using the
spawnkeyword. - Synchronization with WaitGroups: Using
sync.WaitGroupto track and coordinate concurrent worker completion. - Monitor Thread Pattern: Spawning a monitor thread to wait for workers to finish and close the results channel, avoiding channel deadlocks.
- Graceful Shutdown: Closing the tasks channel (
tasks_chan.close()) to signal worker threads to cleanly exit.
module main
import time
import sync
// Task represents the unit of work to be processed.
struct Task {
id int
data string
}
// Result represents the outcome of processing a Task.
struct Result {
task_id int
worker_id int
output string
duration time.Duration
}
// worker runs in a separate thread, consuming from tasks_chan and producing to results_chan.
fn worker(id int, tasks_chan chan Task, results_chan chan Result, mut wg sync.WaitGroup) {
defer {
wg.done()
}
for {
// Receive a task from the channel.
// If the channel is closed and empty, it returns `none`
t := <-tasks_chan or { break }
start_time := time.now()
// Simulate intensive processing/I/O task
time.sleep(50 * time.millisecond)
elapsed := time.since(start_time)
// Send the result to the output channel
results_chan <- Result{
task_id: t.id
worker_id: id
output: 'Processed: ' + t.data.to_upper()
duration: elapsed
}
}
}
// wait_and_close waits for all workers to finish and then closes the results channel.
fn wait_and_close(mut wg sync.WaitGroup, results_chan chan Result) {
wg.wait()
results_chan.close()
}
fn main() {
println('=== V Worker Pool Concurrency Boilerplate ===')
// 1. Create channels for tasks and results with capacities
tasks_chan := chan Task{cap: 10}
results_chan := chan Result{cap: 10}
num_workers := 3
num_tasks := 5
mut wg := sync.new_waitgroup()
// 2. Spawn concurrent worker threads
println('Spawning ${num_workers} workers...')
for i in 0 .. num_workers {
wg.add(1)
spawn worker(i + 1, tasks_chan, results_chan, mut wg)
}
// Spawn the monitor thread to close results_chan when all workers are done
spawn wait_and_close(mut wg, results_chan)
// 3. Dispatch tasks to the queue
println('Dispatching ${num_tasks} tasks to worker pool...')
for i in 0 .. num_tasks {
tasks_chan <- Task{
id: i + 1
data: 'task-payload-${i + 1}'
}
}
// 4. Close tasks channel to signal workers that no more work is coming
tasks_chan.close()
println('Tasks dispatched, queue closed. Collecting results...')
// 5. Collect results from results channel by iterating until it is closed
mut results := []Result{}
for {
res := <-results_chan or { break }
results << res
println('Received: Task #${res.task_id} from Worker #${res.worker_id} (took ${res.duration.milliseconds()}ms)')
}
// 6. Print summary
println('\n=== Processing Summary ===')
for res in results {
println('- Task #${res.task_id} -> ${res.output} (Worker #${res.worker_id})')
}
}OS and File Utilities Boilerplate
In plain words: A practical toolkit template for reading, writing, copying, backing up, and scanning files and directories safely.
V's standard os module contains comprehensive and platform-agnostic tools for interacting with the file system and host operating system.
Key concepts illustrated:
- Path Manipulation: Using
os.join_pathto build paths correctly across Linux, macOS, and Windows. - File I/O: Performing quick reads and writes using
os.read_fileandos.write_file. - Directory Operations: Checking folder existence, recursively creating folders via
os.mkdir_all, and listing files viaos.ls. - Metadata Inspection: Interrogating file properties like sizes and types (
os.is_file/os.is_dir). - Resource Cleanup: Ensuring system hygiene by safely removing files (
os.rm) and folders (os.rmdir).
module main
import os
fn main() {
println('=== V OS & File Utilities Boilerplate ===')
// 1. Join paths safely across different operating systems using os.join_path
// V's home_dir() gives the user's home directory. Let's use it as a base path inside a temporary subfolder in workspace.
cwd := os.getwd()
temp_dir := os.join_path(cwd, 'temp_file_demo')
println('Target directory: ${temp_dir}')
// 2. Check if a directory exists, and create it recursively if not
if !os.exists(temp_dir) {
println('Directory does not exist. Creating...')
os.mkdir_all(temp_dir) or {
eprintln('Failed to create directory: ${err}')
exit(1)
}
}
target_file := os.join_path(temp_dir, 'sample.txt')
println('Target file path: ${target_file}')
// 3. Write data to a file (overwrites if it already exists)
content_to_write := 'Hello V Developers!\nThis is a sample file created by the OS and File utilities boilerplate.'
os.write_file(target_file, content_to_write) or {
eprintln('Failed to write to file: ${err}')
exit(1)
}
println('File written successfully.')
// 4. Read data from a file back into memory
read_content := os.read_file(target_file) or {
eprintln('Failed to read file: ${err}')
exit(1)
}
println('\n--- Read Content ---')
println(read_content)
println('--------------------\n')
// 5. Query file metadata
size := os.file_size(target_file)
is_file := os.is_file(target_file)
is_dir := os.is_dir(temp_dir)
println('File properties:')
println('- Size: ${size} bytes')
println('- Is File: ${is_file}')
println('- Is Directory: ${is_dir}')
// 6. List all files and folders inside a directory
println('\nListing contents of directory: ${temp_dir}')
files := os.ls(temp_dir) or { []string{} }
for file in files {
full_path := os.join_path(temp_dir, file)
file_type := if os.is_dir(full_path) { '[DIR]' } else { '[FILE]' }
println(' ${file_type} ${file}')
}
// 7. Clean up by deleting the file and directory
println('\nCleaning up temporary files and directories...')
os.rm(target_file) or { eprintln('Failed to delete file: ${err}') }
os.rmdir(temp_dir) or { eprintln('Failed to delete directory: ${err}') }
println('Cleanup completed successfully.')
}String Utilities Boilerplate
In plain words: A collection of ready-to-use text helpers for slugifying titles, counting words, masking sensitive strings, and formatting tables.
This boilerplate demonstrates how to implement highly useful custom string operations that are not natively built into V's core string type. It highlights UTF-8 rune handling, string tokenization, filtering, and text manipulation.
Key concepts illustrated:
- UTF-8 Safe String Reversal: Reversing strings correctly by iterating over
runesrather than raw bytes, ensuring multi-byte Unicode characters (like emojis) are not corrupted. - Title Casing: Capitalizing the first letter of each word in a sentence (V's native
.capitalize()only capitalizes the first letter of the entire string). - Alphanumeric Palindrome Checking: Checking if a string is a palindrome while ignoring non-alphanumeric symbols and character casing.
- Rune-based Truncation: Cutting off a string at a specific character limit and adding an ellipsis, avoiding splitting UTF-8 byte sequences.
- Slugification: Generating clean, URL-friendly slugs (e.g. low-cased, hyphen-separated, special characters stripped) from user-supplied titles.
module main
// reverse_string reverses a string, properly handling multi-byte UTF-8 characters (runes).
fn reverse_string(s string) string {
runes := s.runes()
mut rev_runes := []rune{cap: runes.len}
for i := runes.len - 1; i >= 0; i-- {
rev_runes << runes[i]
}
return rev_runes.string()
}
// title_case capitalizes the first letter of each word in a string.
fn title_case(s string) string {
words := s.split(' ')
mut titled_words := []string{cap: words.len}
for word in words {
if word.len == 0 {
titled_words << ''
continue
}
titled_words << word.capitalize()
}
return titled_words.join(' ')
}
// is_palindrome checks if a string reads the same forwards and backwards,
// ignoring case and non-alphanumeric characters.
fn is_palindrome(s string) bool {
// Filter to lowercase alphanumeric characters
mut clean_chars := []rune{}
for r in s.to_lower().runes() {
if (r >= `a` && r <= `z`) || (r >= `0` && r <= `9`) {
clean_chars << r
}
}
for i in 0 .. clean_chars.len / 2 {
if clean_chars[i] != clean_chars[clean_chars.len - 1 - i] {
return false
}
}
return true
}
// truncate cuts off a string at a specified limit (by rune count) and appends an ellipsis.
fn truncate(s string, limit int) string {
runes := s.runes()
if runes.len <= limit {
return s
}
return runes[0..limit].string() + '...'
}
// slugify converts a string into a clean, URL-friendly slug.
fn slugify(s string) string {
mut res := []rune{}
mut last_was_dash := false
for r in s.to_lower().runes() {
if (r >= `a` && r <= `z`) || (r >= `0` && r <= `9`) {
res << r
last_was_dash = false
} else if r == ` ` || r == `-` || r == `_` {
if !last_was_dash && res.len > 0 {
res << `-`
last_was_dash = true
}
}
}
// Trim trailing dash if any
mut slug := res.string()
if slug.ends_with('-') {
slug = slug[0..slug.len - 1]
}
return slug
}
fn main() {
println('=== V Custom String Utilities Boilerplate ===')
// 1. Reverse String (UTF-8 safe)
phrase := 'Hello, ๐ World!'
println('Original: "${phrase}"')
println('Reversed: "${reverse_string(phrase)}"')
// 2. Title Case (capitalizing every word)
title := 'v programming language complete textbook guide'
println('\nOriginal: "${title}"')
println('Title Case: "${title_case(title)}"')
// 3. Palindrome Check
pal1 := 'A man, a plan, a canal: Panama!'
pal2 := 'Hello Vlang'
println('\nIs "${pal1}" a palindrome? ${is_palindrome(pal1)}')
println('Is "${pal2}" a palindrome? ${is_palindrome(pal2)}')
// 4. Truncation
long_text := 'V is a statically typed compiled programming language designed for building maintainable software.'
println('\nOriginal: "${long_text}"')
println('Truncated: "${truncate(long_text, 35)}"')
// 5. Slugify
title_to_slug := ' Vlang: Concurrency, Channels, & Web APIs! '
println('\nOriginal: "${title_to_slug}"')
println('Slugified: "${slugify(title_to_slug)}"')
}Math and Statistics Boilerplate
In plain words: Starter functions for calculating averages (mean, median), min/max, standard deviation, and clamping numbers.
This template showcases how to perform numerical operations and custom statistical computations on numeric arrays. It implements several custom algorithmic functions that are not built directly into V's basic numeric types or standard math package.
Key concepts illustrated:
- Descriptive Statistics: Iterating over elements to calculate min, max, sum, and average (mean) on float slices.
- Array Sorting & Cloning: Using
.clone()to copy data and sorting arrays in-place using.sort(). - Median, Variance & Deviation: Custom implementations to calculate distribution variance and standard deviation using V's
math.sqrt(). - Factorial Calculation: An iterative, overflow-aware custom factorial function using V's
!result type to report overflow on inputs greater than 20. - Fibonacci Sequence Generator: A custom dynamic programming function generating the first $N$ numbers of the Fibonacci sequence, using
!to report overflow on inputs greater than 93. - Prime Number Checker: A highly optimized primality test (
is_prime) using trial division of form $6k \pm 1$. - GCD and LCM: Custom implementations for finding the Greatest Common Divisor (Euclidean algorithm) and Least Common Multiple of two numbers.
module main
import math
// Stats holds the computed statistical properties of a dataset.
struct Stats {
count int
min f64
max f64
sum f64
mean f64
median f64
variance f64
std_dev f64
}
// calculate_stats calculates standard descriptive statistics on a float dataset (not built into V arrays).
fn calculate_stats(numbers []f64) ?Stats {
if numbers.len == 0 {
return none
}
mut sum := 0.0
mut min := numbers[0]
mut max := numbers[0]
for val in numbers {
sum += val
if val < min {
min = val
}
if val > max {
max = val
}
}
mean := sum / numbers.len
// Calculate median (requires a sorted copy of the numbers)
mut sorted := numbers.clone()
sorted.sort()
mut median := 0.0
mid := sorted.len / 2
if sorted.len % 2 == 0 {
median = (sorted[mid - 1] + sorted[mid]) / 2.0
} else {
median = sorted[mid]
}
// Calculate variance and standard deviation
mut variance_sum := 0.0
for val in numbers {
diff := val - mean
variance_sum += diff * diff
}
variance := variance_sum / numbers.len
std_dev := math.sqrt(variance)
return Stats{
count: numbers.len
min: min
max: max
sum: sum
mean: mean
median: median
variance: variance
std_dev: std_dev
}
}
// factorial calculates the factorial of a number iteratively, with overflow checks.
fn factorial(n int) !u64 {
if n < 0 {
return error('Factorial is not defined for negative numbers')
}
if n > 20 {
return error('Factorial of ${n} overflows 64-bit unsigned integer limit (max n is 20)')
}
mut result := u64(1)
for i in 2 .. n + 1 {
result *= u64(i)
}
return result
}
// fibonacci generates the first n Fibonacci numbers, with overflow checks.
fn fibonacci(n int) ![]u64 {
if n < 0 {
return error('Count must be non-negative')
}
if n > 93 {
return error('Fibonacci sequence beyond 93 elements overflows 64-bit unsigned integer limit')
}
if n == 0 {
return []u64{}
}
if n == 1 {
return [u64(0)]
}
mut sequence := []u64{cap: n}
sequence << u64(0)
sequence << u64(1)
for i in 2 .. n {
sequence << sequence[i - 1] + sequence[i - 2]
}
return sequence
}
// is_prime checks if a number is prime.
fn is_prime(n int) bool {
if n <= 1 {
return false
}
if n <= 3 {
return true
}
if n % 2 == 0 || n % 3 == 0 {
return false
}
mut i := 5
for i * i <= n {
if n % i == 0 || n % (i + 2) == 0 {
return false
}
i += 6
}
return true
}
// gcd computes the Greatest Common Divisor of two integers.
fn gcd(a int, b int) int {
mut x := math.abs(a)
mut y := math.abs(b)
for y != 0 {
temp := y
y = x % y
x = temp
}
return x
}
// lcm computes the Least Common Multiple of two integers.
fn lcm(a int, b int) int {
if a == 0 || b == 0 {
return 0
}
return (math.abs(a) * math.abs(b)) / gcd(a, b)
}
fn main() {
println('=== V Custom Math & Statistics Boilerplate ===')
// 1. Descriptive Statistics Demo
data := [72.5, 81.0, 68.5, 90.0, 75.5, 78.0, 85.5]
println('Dataset: ${data}')
stats := calculate_stats(data) or {
println('Error: Empty dataset')
return
}
println('\nStatistical Results:')
println('- Count: ${stats.count}')
println('- Minimum: ${stats.min:.2f}')
println('- Maximum: ${stats.max:.2f}')
println('- Sum: ${stats.sum:.2f}')
println('- Mean (Average): ${stats.mean:.2f}')
println('- Median: ${stats.median:.2f}')
println('- Variance: ${stats.variance:.2f}')
println('- Standard Deviation: ${stats.std_dev:.2f}')
// 2. Custom Math Functions Demo
n := 10
println('\nCustom Number Functions:')
fact := factorial(n) or {
eprintln('Error: ${err}')
u64(0)
}
println('- Factorial of ${n}: ${fact}')
fib := fibonacci(n) or {
eprintln('Error: ${err}')
[]u64{}
}
println('- Fibonacci first ${n}: ${fib}')
test_primes := [7, 12, 19, 25, 97]
for p in test_primes {
println(' Is ${p} prime? ${is_prime(p)}')
}
a, b := 24, 36
println('\nCommon Number Relations:')
println('- GCD of ${a} and ${b}: ${gcd(a, b)}')
println('- LCM of ${a} and ${b}: ${lcm(a, b)}')
}Array Utilities Boilerplate
In plain words: Reusable generic list helpers for removing duplicates, chunking arrays into pages, and finding intersections.
V's standard array implementation is powerful but focused. This template showcases how to implement useful custom generic utilities for manipulating, comparing, and organizing arrays.
Key concepts illustrated:
- Generics in V: Writing reusable algorithms using type parameters
[T]. - Deduplication (
unique): Removing duplicates from an array using containment checks (!in). - Chunking (
chunk): Partitioning an array into smaller sub-slices of a fixed maximum length. - Set Operations (
intersection,difference): Calculating overlapping elements and unique elements between two arrays. - Flattening (
flatten): Condensing nested 2D slices ([][]T) into flat 1D slices ([]T). - In-place Shuffling (
shuffle): Implementing the Fisher-Yates shuffle algorithm using V's standardrandpackage. - Weighted Selection (
weighted_choice): Selecting elements from an array based on proportional bias (weights) to simulate probability distributions.
module main
import rand
// unique returns a new array with duplicate elements removed.
fn unique[T](arr []T) []T {
mut result := []T{cap: arr.len}
for item in arr {
if item !in result {
result << item
}
}
return result
}
// chunk splits an array into sub-arrays of the specified size.
fn chunk[T](arr []T, size int) [][]T {
if size <= 0 || arr.len == 0 {
return [][]T{}
}
mut result := [][]T{}
mut current_chunk := []T{}
for item in arr {
current_chunk << item
if current_chunk.len == size {
result << current_chunk
current_chunk = []T{}
}
}
if current_chunk.len > 0 {
result << current_chunk
}
return result
}
// intersection returns a new array containing elements present in both arrays.
fn intersection[T](a []T, b []T) []T {
mut result := []T{}
for item in a {
if item in b && item !in result {
result << item
}
}
return result
}
// difference returns a new array containing elements present in a but not in b.
fn difference[T](a []T, b []T) []T {
mut result := []T{}
for item in a {
if item !in b && item !in result {
result << item
}
}
return result
}
// flatten flattens a 2D array into a 1D array.
fn flatten[T](arr [][]T) []T {
mut result := []T{}
for sub_arr in arr {
for item in sub_arr {
result << item
}
}
return result
}
// shuffle randomizes the order of elements in-place using the Fisher-Yates algorithm.
fn shuffle[T](mut arr []T) {
for i := arr.len - 1; i > 0; i-- {
j := rand.intn(i + 1) or { 0 }
temp := arr[i]
arr[i] = arr[j]
arr[j] = temp
}
}
// weighted_choice returns an element from `values` based on their corresponding `weights`.
// The lengths of `values` and `weights` must be equal and non-zero.
// All weights must be non-negative.
fn weighted_choice[T](values []T, weights []int) !T {
if values.len != weights.len {
return error('values and weights must have the same length')
}
if values.len == 0 {
return error('cannot pick from empty arrays')
}
mut total_weight := 0
for w in weights {
if w < 0 {
return error('weights must be non-negative')
}
total_weight += w
}
if total_weight <= 0 {
return error('sum of weights must be greater than zero')
}
// Pick a random number in [0, total_weight)
r := rand.intn(total_weight) or { return error('failed to generate random number: ${err}') }
mut running_sum := 0
for i, w in weights {
running_sum += w
if r < running_sum {
return values[i]
}
}
return values[values.len - 1]
}
fn main() {
println('=== V Custom Array Utilities Boilerplate ===')
// 1. Unique / Deduplication Demo
duplicates := [1, 2, 2, 3, 1, 4, 3, 5, 2]
println('Original: ${duplicates}')
println('Deduplicated: ${unique(duplicates)}')
// 2. Chunking Demo
to_chunk := ['a', 'b', 'c', 'd', 'e', 'f', 'g']
chunk_size := 3
println('\nOriginal: ${to_chunk}')
println('Chunked (${chunk_size}): ${chunk[string](to_chunk, chunk_size)}')
// 3. Intersection & Difference Demo
arr_a := [1, 2, 3, 4, 5]
arr_b := [4, 5, 6, 7, 8]
println('\nArray A: ${arr_a}')
println('Array B: ${arr_b}')
println('Intersection: ${intersection(arr_a, arr_b)}')
println('Difference: ${difference(arr_a, arr_b)}')
// 4. Flattening Demo
nested := [[1, 2], [3, 4, 5], [6]]
println('\nNested: ${nested}')
println('Flattened: ${flatten(nested)}')
// 5. In-place Shuffling Demo
mut to_shuffle := [10, 20, 30, 40, 50, 60, 70]
println('\nBefore Shuffle: ${to_shuffle}')
shuffle(mut to_shuffle)
println('After Shuffle: ${to_shuffle}')
// 6. Weighted Random Choice Demo (picking with bias)
items := ['Common', 'Uncommon', 'Rare', 'Legendary']
weights := [70, 20, 9, 1] // Sum is 100
println('\nWeighted Choice Demo (picking with bias):')
println('Items: ${items}')
println('Weights: ${weights}')
// Simulate 10,000 picks to demonstrate that the distribution matches the weights
mut stats := map[string]int{}
for _ in 0 .. 10000 {
picked := weighted_choice[string](items, weights) or {
eprintln('Error: ${err}')
continue
}
stats[picked]++
}
println('Simulation results (out of 10,000 picks):')
for item in items {
println('- ${item}: ${stats[item]} picks (~${(f64(stats[item]) / 100.0):.1f}%)')
}
}Configuration Management Boilerplate
In plain words: Shows how to load application settings from environment variables and JSON config files with safe fallback defaults.
Most practical applications need configuration values that can come from a file, environment variables, or defaults. This template shows a clean pattern for loading a JSON config, falling back to safe defaults, and saving updated settings.
Key concepts illustrated:
- Default Configuration: Establishing a safe baseline before reading external settings.
- Environment Variable Overrides: Adapting the app without changing source files.
- JSON Persistence: Reading and writing structured config files with
json2.decodeandjson2.encode. - Practical Separation of Concerns: Keeping parsing and application logic in dedicated functions.
module main
import json2
import os
struct AppConfig {
mut:
host string
port int
debug bool
retries int
}
fn default_config() AppConfig {
return AppConfig{
host: '127.0.0.1'
port: 8080
debug: false
retries: 3
}
}
fn load_config(path string) AppConfig {
mut cfg := default_config()
// 1. If path is provided and exists, load config from the JSON file first
if path != '' && os.exists(path) {
raw := os.read_file(path) or {
eprintln('Warning: could not read config file: ${err}')
''
}
if raw != '' {
cfg = json2.decode[AppConfig](raw) or {
eprintln('Warning: could not decode config file: ${err}')
cfg
}
}
} else if path != '' {
println('Config file not found. Using defaults with environment overrides.')
}
// 2. Overlay / override with environment variables (crucial for production container envs)
env_host := os.getenv('APP_HOST')
if env_host != '' {
cfg.host = env_host
}
env_port := os.getenv('APP_PORT')
if env_port != '' {
cfg.port = env_port.int()
}
env_debug := os.getenv('APP_DEBUG')
if env_debug != '' {
cfg.debug = env_debug == 'true'
}
env_retries := os.getenv('APP_RETRIES')
if env_retries != '' {
cfg.retries = env_retries.int()
}
return cfg
}
fn save_config(path string, cfg AppConfig) {
data := json2.encode(cfg)
os.write_file(path, data) or { eprint('Failed to save config file: ${err}') }
}
fn main() {
println('=== V Configuration Management Boilerplate ===')
config_path := 'app_config.json'
// Ensure we cleanup the generated file on exit
defer {
if os.exists(config_path) {
os.rm(config_path) or {}
println('Cleaned up temporary config file: ${config_path}')
}
}
mut cfg := load_config(config_path)
println('Loaded configuration:')
println('- host: ${cfg.host}')
println('- port: ${cfg.port}')
println('- debug: ${cfg.debug}')
println('- retries: ${cfg.retries}')
cfg.debug = true
save_config(config_path, cfg)
println('Saved configuration to ${config_path}')
}JSON File Store Boilerplate
In plain words: A lightweight local database template that stores and queries structs inside a JSON file on disk.
A simple JSON file store is often enough for small tools, prototypes, or local productivity apps. This template shows how to load persisted data from disk, add new records, update them, and save the state again.
Key concepts illustrated:
- Persistent Data: Keeping application state across runs with a simple file-backed format.
- Structured Records: Storing typed data in a
TodoItemmodel. - CRUD Style Helpers: Adding, updating, and listing records without introducing a database dependency.
- Safe Serialization: Using
json2.encodeandjson2.decodefor portability.
module main
import json2
import os
struct TodoItem {
id int
title string
mut:
done bool
}
struct TodoStore {
mut:
items []TodoItem
}
fn load_store(path string) TodoStore {
if !os.exists(path) {
return TodoStore{}
}
raw := os.read_file(path) or {
eprintln('Could not read store: ${err}')
return TodoStore{}
}
decoded := json2.decode[TodoStore](raw) or {
eprintln('Could not decode store: ${err}')
return TodoStore{}
}
return decoded
}
fn save_store(path string, store TodoStore) {
data := json2.encode(store)
os.write_file(path, data) or { eprintln('Could not save store: ${err}') }
}
fn add_item(mut store TodoStore, title string) TodoItem {
item := TodoItem{
id: store.items.len + 1
title: title
done: false
}
store.items << item
return item
}
fn mark_done(mut store TodoStore, id int) bool {
for i, item in store.items {
if item.id == id {
store.items[i].done = true
return true
}
}
return false
}
fn list_items(store TodoStore) {
for item in store.items {
status := if item.done { '[x]' } else { '[ ]' }
println('${status} ${item.id}. ${item.title}')
}
}
fn main() {
println('=== V JSON File Store Boilerplate ===')
store_path := 'todos.json'
// Ensure we cleanup the generated file on exit
defer {
if os.exists(store_path) {
os.rm(store_path) or {}
println('Cleaned up temporary store file: ${store_path}')
}
}
mut store := load_store(store_path)
add_item(mut store, 'Write a V tutorial')
add_item(mut store, 'Ship a new boilerplate example')
println('Current todos:')
list_items(store)
if mark_done(mut store, 1) {
println('Marked todo #1 as done.')
} else {
println('Todo #1 was not found.')
}
save_store(store_path, store)
println('Saved todos to ${store_path}')
}Retry and Backoff Boilerplate
In plain words: A resilient helper template that automatically retries a flaky network or file operation with a short pause (exponential backoff) between tries.
Transient failures are common in distributed systems, network clients, and file operations. This template demonstrates a practical retry loop that delays between attempts and surfaces a final failure only after the allowed number of tries is exhausted.
Key concepts illustrated:
- Resilient Operations: Using a generic function structure to wrap any callback in a robust retry cycle.
- Exponential Backoff: Multiplying delays by a backoff factor after each failed attempt to reduce load on resources.
- Random Jitter: Adding a small, random time variation (jitter) to sleep intervals to prevent synchronized retries (thundering herd problem).
- Result & Error Propagation: Leveraging V's
!result type to return either the successful generic valueTor propagate the final failure.
module main
import os
import time
import rand
// RetryConfig configures the retry and backoff behavior.
struct RetryConfig {
attempts int = 3
initial_delay time.Duration = 100 * time.millisecond
factor f64 = 2.0
max_delay time.Duration = 3 * time.second
jitter bool = true
}
// retry executes the operation `op` up to `cfg.attempts` times.
// It uses exponential backoff with optional random jitter.
fn retry[T](cfg RetryConfig, op fn () !T) !T {
mut delay := cfg.initial_delay
for attempt in 1 .. cfg.attempts + 1 {
res := op() or {
if attempt == cfg.attempts {
return error('Operation failed after ${cfg.attempts} attempts. Last error: ${err}')
}
eprintln('Attempt ${attempt}/${cfg.attempts} failed: ${err}. Retrying in ${delay.milliseconds()}ms...')
// Sleep with optional jitter to prevent thundering herd problems
mut sleep_dur := delay
if cfg.jitter {
jitter_ms := rand.intn(100) or { 0 }
sleep_dur += jitter_ms * time.millisecond
}
time.sleep(sleep_dur)
// Increase the delay for the next attempt, up to max_delay
delay = time.Duration(i64(f64(delay) * cfg.factor))
if delay > cfg.max_delay {
delay = cfg.max_delay
}
continue
}
return res
}
return error('Unreachable')
}
fn main() {
println('=== V Retry & Backoff Boilerplate ===')
// Create a dummy file to read successfully on the 3rd attempt
file_path := 'temp_retry_demo.txt'
defer {
if os.exists(file_path) {
os.rm(file_path) or {}
}
}
// Spawn a thread to create the file after a short delay
spawn fn [file_path] () {
time.sleep(300 * time.millisecond)
os.write_file(file_path, 'Success: Data retrieved from temporary file!') or {}
println('[System] File created on disk.')
}()
// Define our retry configuration
cfg := RetryConfig{
attempts: 4
initial_delay: 150 * time.millisecond
factor: 1.5
jitter: true
}
// Define the retriable operation closure
op := fn [file_path] () !string {
if !os.exists(file_path) {
return error('File does not exist yet')
}
return os.read_file(file_path)
}
// Run the retry loop
println('Starting resilient file read operation...')
content := retry[string](cfg, op) or {
eprintln('Final failure: ${err}')
return
}
println('\nOperation Succeeded!')
println('Read content: "${content}"')
}HTTP Client Boilerplate
In plain words: A clean starter template for calling external REST APIs, setting headers, and handling HTTP status codes and JSON responses.
Many real-world tools need to call web services. This template shows a simple yet practical pattern for performing HTTP GET and POST requests, decoding JSON responses, and handling failures with clear errors.
Key concepts illustrated:
- HTTP Requests: Using
net.httpfor GET and POST requests. - JSON POST Headers: Using
http.Requestdirectly to configure headers explicitly, includingContent-Type: application/jsonfor strict APIs. - JSON Payloads: Encoding and decoding structured data with
json. - Error Handling: Returning and surfacing client errors cleanly.
- Reusable Helpers: Keeping request logic in dedicated functions for later reuse.
module main
import net.http
import json2
struct PostPayload {
title string @[json: 'title']
body string @[json: 'body']
user_id int @[json: 'userId']
}
struct PostResponse {
id int @[json: 'id']
title string @[json: 'title']
body string @[json: 'body']
user_id int @[json: 'userId']
}
fn fetch_json(url string) !string {
resp := http.get(url) or { return error('GET request failed: ${err}') }
if resp.status_code >= 400 {
return error('Request failed with status ${resp.status_code}')
}
return resp.body
}
fn post_json(url string, payload PostPayload) !PostResponse {
body := json2.encode(payload)
// Set Content-Type explicitly for compliance with strict JSON APIs
mut req := http.Request{
method: .post
url: url
data: body
}
req.header.set(.content_type, 'application/json')
resp := req.do() or { return error('POST request failed: ${err}') }
if resp.status_code >= 400 {
return error('Request failed with status ${resp.status_code}')
}
return json2.decode[PostResponse](resp.body) or { return error('Invalid JSON response') }
}
fn main() {
println('=== V HTTP Client Boilerplate ===')
body := fetch_json('https://httpbin.org/get') or {
eprintln('${err}')
return
}
println('GET response body:')
println(body)
response := post_json('https://jsonplaceholder.typicode.com/posts', PostPayload{
title: 'Ada'
body: 'Developer'
user_id: 1
}) or {
eprintln('${err}')
return
}
println('\nPOST response:')
println('id: ${response.id}')
println('title: ${response.title}')
println('body: ${response.body}')
println('user_id: ${response.user_id}')
}CSV Processor Boilerplate
In plain words: Reads, filters, summarizes, and writes spreadsheet .csv files using V's built-in encoding.csv module.
CSV files are still common for importing and exporting tabular data. This template shows how to read records from CSV, transform them into typed structs, and write them back out in a clean format.
Key concepts illustrated:
- CSV Parsing: Reading rows with
encoding.csv. - Typed Data Models: Mapping each row into a struct.
- File I/O: Reading and writing files with
oshelpers. - Practical Data Pipelines: Transforming input files into processed output files.
module main
import os
import encoding.csv
struct Person {
name string
age int
city string
}
fn read_people(path string) ![]Person {
content := os.read_file(path) or { return error('Could not read ${path}: ${err}') }
mut reader := csv.new_reader(content)
mut people := []Person{}
for {
row := reader.read() or { break }
if row.len == 0 {
continue
}
if row[0] == 'name' {
continue
}
people << Person{
name: row[0]
age: row[1].int()
city: row[2]
}
}
return people
}
fn write_people(path string, people []Person) ! {
mut output := []string{}
output << 'name,age,city'
for person in people {
output << '${person.name},${person.age},${person.city}'
}
os.write_file(path, output.join('\n')) or { return error('Could not write ${path}: ${err}') }
}
fn main() {
println('=== V CSV Processor Boilerplate ===')
input_path := 'people.csv'
output_path := 'people_out.csv'
// Ensure temporary CSV files are cleaned up on exit
defer {
if os.exists(input_path) {
os.rm(input_path) or {}
}
if os.exists(output_path) {
os.rm(output_path) or {}
}
println('Cleaned up temporary CSV files.')
}
os.write_file(input_path, 'name,age,city\nAlice,30,New York\nBob,25,San Francisco') or {
eprintln('Could not create sample CSV: ${err}')
return
}
people := read_people(input_path) or {
eprintln('${err}')
return
}
for person in people {
println('Loaded: ${person.name} (${person.age}) from ${person.city}')
}
write_people(output_path, people) or {
eprintln('${err}')
return
}
println('Wrote processed CSV to ${output_path}')
}macOS Native GUI Boilerplate
In plain words: Shows how to open a native macOS desktop window with buttons, inputs, and event callbacks using vlang_simplegui.
V is not limited to command-line tools. This starter project shows how to build a native macOS desktop app with a small, beginner-friendly API over Cocoa. It is a practical bridge between simple scripting-style V code and real GUI development.
Key concepts illustrated:
- Native Window Creation: Starting a real macOS window with
simplegui.new_simple_window(...). - Named Controls: Creating labels, text inputs, buttons, checkboxes, sliders, and more with one-line helpers.
- Event-Driven UI: Connecting UI actions with callbacks such as
on_clickandon_change. - State Updates from V: Reading and writing control values with helpers like
get_text,set_text,set_checked, andset_value_int. - Crossing into Desktop Apps: Showing how V can be used for interactive desktop experiences beyond console programs.
module main
import simplegui
fn main() {
mut gui := simplegui.new_simple_window('V Native GUI Demo', 760, 950)
gui.set_title('V Native GUI Demo')
gui.add_label('intro', 'Create controls with one small call')
gui.add_input('name', 'Ada')
gui.add_button('run', 'Run')
gui.on_click('run', on_run_clicked)
gui.run()
}
fn on_run_clicked(mut win &simplegui.SimpleWindow) {
println('run clicked')
name := win.get_text('name')
win.alert('Hello', 'Hello, ${name}!')
}The full project in this repository includes multiple demos and a reusable module implementation in boilerplate_templates/13_simplegui/simplegui/simplegui.v, making it an excellent example for learners who want to move from console programs into desktop interfaces.
For the complete source, demo list, screenshots, and setup instructions, visit the original GitHub repository: https://github.com/codecaine-zz/vlang_simplegui/tree/master
To use the project end to end, open the folder at boilerplate_templates/13_simplegui and change into that directory first so it becomes the current working directory, then run the main demo with v run main.v. This matters because the example expects to be launched from its own folder. You can also explore the other demo files in the same directory to see the available UI patterns.
Logging Boilerplate
In plain words: A structured application logger template supporting info/warn/error levels, timestamps, and file output.
Structured logging is essential for debugging, monitoring, and understanding how an application behaves over time. This template shows a simple logger that writes messages with levels, timestamps, and optional file output.
Key concepts illustrated:
- Log Levels: Using
debug,info,warn, anderrorcategories. - Simple Logger Struct: Encapsulating configuration and behavior in one reusable type.
- File Output: Appending log entries to a file for later inspection.
- Filtering: Only emitting messages at or above the configured severity level.
module main
import os
import time
enum LogLevel {
debug
info
warn
error
}
struct Logger {
log_file string
level LogLevel
}
fn (logger Logger) log(level LogLevel, message string) {
if int(level) < int(logger.level) {
return
}
timestamp := time.now().str()
prefix := match level {
.debug { '[DEBUG]' }
.info { '[INFO]' }
.warn { '[WARN]' }
.error { '[ERROR]' }
}
line := '${timestamp} ${prefix} ${message}'
println(line)
if logger.log_file != '' {
mut f := os.open_file(logger.log_file, 'a') or {
eprintln('Failed to open log file: ${err}')
return
}
f.write((line + '\n').bytes()) or { eprintln('Failed to append log: ${err}') }
f.close()
}
}
fn main() {
println('=== V Logging Boilerplate ===')
log_path := 'app.log'
defer {
if os.exists(log_path) {
os.rm(log_path) or {}
println('Cleaned up temporary log file: ${log_path}')
}
}
logger := Logger{
log_file: log_path
level: .info
}
logger.log(.debug, 'This debug message is filtered out')
logger.log(.info, 'Application started')
logger.log(.warn, 'Configuration value is missing')
logger.log(.error, 'Something went wrong')
}Featured Real-World V Applications, GUI Frameworks & Game Suites
Once you are comfortable with the boilerplate templates above, explore these full-scale open-source V frameworks, visual studios, and game suites created by @codecaine-zz to see how V scales to large, production desktop and gaming projects:
1. Cross-Platform Hardware-Accelerated UI (`simple_gg`)
Repository: https://github.com/codecaine-zz/simple_gg
In Plain English: simple_gg lets you build fast, smooth desktop apps that look and work the same on macOS, Linux, and Windows. Instead of bundling a heavy 200 MB web browser like Electron, it draws buttons, charts, and windows directly on your computer's graphics card using V's built-in gg moduleโresulting in tiny (~1.4 MB) apps that start instantly and run at 120 FPS.
2. Native macOS Cocoa GUI & Visual Designer (`vlang_simplegui`)
Repository: https://github.com/codecaine-zz/vlang_simplegui
In Plain English: If you want to build real macOS apps with native Apple buttons, menus, tables, and windows without learning complex Objective-C, vlang_simplegui gives you 115+ simple V controls (win.add_button(...), win.add_input(...)), 140+ runnable demos, and a drag-and-drop Visual UI Designer inspired by Delphi and Visual Basic.
3. Cross-Platform Webview Visual RAD Studio (`vlang_webview_rad_studio`)
Repository: https://github.com/codecaine-zz/vlang_webview_rad_studio
In Plain English: Combines the speed of a native V backend with the styling flexibility of your operating system's built-in Webview (macOS WebKit, Windows Edge WebView2, Linux WebKit2GTK). Includes a full drag-and-drop visual form designer, property inspector, 70+ UI components, 76 themes, and native system telemetry tools.
4. 88+ Playable 2D & 3D Arcade Games in V (`vlang_sdl2_games` & `sdl_games`)
- Sprite & Real Audio Edition: https://github.com/codecaine-zz/vlang_sdl2_games
- Zero-Asset Procedural Edition: https://github.com/codecaine-zz/sdl_games
In Plain English: Want to learn game programming in V? These two repositories contain 88+ complete, playable arcade games, retro classics, puzzle games, board games, and physics simulations written entirely in V using vlang/sdl:
- Start with
sdl_gamesto see how to build complete games with zero external filesโevery sound effect and graphic is drawn and synthesized using pure V math! - Explore
vlang_sdl2_gamesto see how to load PNG sprite sheets, play 16-bit WAV audio effects and background music, and save/load game states (F5/F9) across macOS, Linux, and Windows.
Chapter 15 Comprehensive Practice Exercises
Plain-English Chapter Guide (Start Here if You Are New to Coding!)
- The Big Idea in Simple Words: The best way to learn programming is by doing! Each exercise below gives you a small, fun goal based on one of the chapters you just read. Try writing the code yourself first (or in the V Playground), and then click to open the Solution and Output to check your work!
Quick Access โ Jump to any lesson in this chapter
Below is an index of all exercises in this chapter. You can use these links to jump directly to any specific exercise:
Practice Exercises Overview
- Exercise 1: Comments and Console Printing
- Exercise 2: Circle Area Calculator with Variables and Constants
- Exercise 3: String and Rune Processing
- Exercise 4: Custom FizzBuzz with Match
- Exercise 5: Filtering and Sorting Student Grades
- Exercise 6: Higher-Order Functions with Callbacks
- Exercise 7: Modeling a Bank Account with Structs & Methods
- Exercise 8: Safe Division with Option/Result
- Exercise 9: Modular Math Utility Project
- Exercise 10: Unit Testing String Reversal
- Exercise 11: Concurrent Task Aggregation with Channels
- Exercise 12: JSON Parsing and Validation
- Exercise 13: HTTP Client & Query Parameter Parser
- Exercise 14: Concurrent Worker Pool for String Transformation
Practice Exercises Overview
Welcome to Chapter 15! This chapter contains comprehensive practice exercises designed to consolidate your learning. Each exercise targets a specific chapter from this book and includes the prompt, the correct V code solution, and the expected terminal output.
Use these exercises to test your understanding as you progress through each chapter of the textbook.
Exercise 1: Comments and Console Printing
This exercise covers Chapter 1: Getting Started with V.
Write a V program that prints your favorite programming languages to the console, utilizing single-line comments for metadata (such as Author and Date) and a multi-line comment describing the compile-to-C architecture of V.
SOLUTION (Click to Reveal / Hide)
module main
// Author: V Learner
// Date: June 2026
fn main() {
/*
This program prints a list of favorite languages.
V compiles to C, resulting in fast execution and tiny binaries.
*/
println('My favorite programming languages are:')
println('- V (for speed and simplicity)')
println('- Go (for cloud networking)')
println('- Rust (for memory safety)')
}EXPECTED OUTPUT (Click to Reveal / Hide)
My favorite programming languages are:
- V (for speed and simplicity)
- Go (for cloud networking)
- Rust (for memory safety)Exercise 2: Circle Area Calculator with Variables and Constants
This exercise covers Chapter 2: Variables and Constants.
Create a program that defines a constant pi = 3.14159. In the main function, declare a mutable variable for the radius of a circle, initialize it to 5.0, and compute/print the area. Then, update the radius to 10.0, recompute the area, and print the updated result with 2 decimal places.
SOLUTION (Click to Reveal / Hide)
module main
const pi = 3.14159
fn main() {
mut radius := 5.0
mut area := pi * radius * radius
println('Radius: ${radius} | Area: ${area:.2f}')
radius = 10.0
area = pi * radius * radius
println('Radius: ${radius} | Area: ${area:.2f}')
}EXPECTED OUTPUT (Click to Reveal / Hide)
Radius: 5 | Area: 78.54
Radius: 10 | Area: 314.16Exercise 3: String and Rune Processing
This exercise covers Chapter 3: Primitive Data Types.
Create a V program that takes a string representation of a username. Retrieve and print its length, convert the entire username to uppercase, extract the first letter as a rune, and print its character representation as well as its raw ASCII integer value.
SOLUTION (Click to Reveal / Hide)
module main
fn main() {
username := 'vlang_developer'
// Get string length
len := username.len
println('Username length: ${len}')
// Convert to uppercase
upper := username.to_upper()
println('Uppercase: ${upper}')
// Extract first character as rune
first_char := username[0]
println('First character: ${first_char.ascii_str()}')
println('ASCII value: ${int(first_char)}')
}EXPECTED OUTPUT (Click to Reveal / Hide)
Username length: 15
Uppercase: VLANG_DEVELOPER
First character: v
ASCII value: 118Exercise 4: Custom FizzBuzz with Match
This exercise covers Chapter 4: Control Flow.
Write a V program that loops from 1 to 20. For each number, determine if it is divisible by 3, 5, both, or neither. Use a match expression to print "Fizz" for multiples of 3, "Buzz" for multiples of 5, "FizzBuzz" for multiples of both, and the number itself otherwise.
SOLUTION (Click to Reveal / Hide)
module main
fn main() {
for i in 1 .. 21 {
match true {
i % 15 == 0 { println('FizzBuzz') }
i % 3 == 0 { println('Fizz') }
i % 5 == 0 { println('Buzz') }
else { println(i) }
}
}
}EXPECTED OUTPUT (Click to Reveal / Hide)
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
16
17
Fizz
19
BuzzExercise 5: Filtering and Sorting Student Grades
This exercise covers Chapter 5: Collections: Arrays and Maps.
Create a map storing student names and their corresponding numeric grades. Filter the map to extract all students who scored 80 or above. Store these students' names in an array, sort the array alphabetically, and print the sorted names.
SOLUTION (Click to Reveal / Hide)
module main
fn main() {
grades := {
'Alice': 85
'Bob': 72
'Charlie': 90
'Diana': 65
'Ethan': 88
}
mut top_students := []string{}
for name, grade in grades {
if grade >= 80 {
top_students << name
}
}
top_students.sort()
println('Top Students (Alphabetical): ${top_students}')
}EXPECTED OUTPUT (Click to Reveal / Hide)
Top Students (Alphabetical): ['Alice', 'Charlie', 'Ethan']Exercise 6: Higher-Order Functions with Callbacks
This exercise covers Chapter 6: Functions.
Write a function filter_ints(nums []int, f fn (int) bool) []int that filters an array of integers using a callback function. In main, call filter_ints once to filter even numbers, and once to filter numbers greater than 10. Print the results.
SOLUTION (Click to Reveal / Hide)
module main
fn filter_ints(nums []int, f fn (int) bool) []int {
mut result := []int{}
for num in nums {
if f(num) {
result << num
}
}
return result
}
fn is_even(n int) bool {
return n % 2 == 0
}
fn main() {
numbers := [2, 5, 12, 7, 18, 9, 3, 22]
evens := filter_ints(numbers, is_even)
println('Even numbers: ${evens}')
greater_than_ten := filter_ints(numbers, fn (n int) bool {
return n > 10
})
println('Numbers > 10: ${greater_than_ten}')
}EXPECTED OUTPUT (Click to Reveal / Hide)
Even numbers: [2, 12, 18, 22]
Numbers > 10: [12, 18, 22]Exercise 7: Modeling a Bank Account with Structs & Methods
This exercise covers Chapter 7: Structs (Custom Types).
Define a BankAccount struct with fields owner (string), balance (f64), and is_active (bool). Implement a value receiver method to display the account details, and mutable methods to deposit(amount f64) and withdraw(amount f64). Ensure that a withdrawal cannot exceed the balance or occur on an inactive account.
SOLUTION (Click to Reveal / Hide)
module main
struct BankAccount {
owner string
mut:
balance f64
is_active bool
}
fn (a BankAccount) display() {
status := if a.is_active { 'Active' } else { 'Inactive' }
println('Account Owner: ${a.owner} | Balance: $${a.balance:.2f} | Status: ${status}')
}
fn (mut a BankAccount) deposit(amount f64) {
if !a.is_active {
println('Cannot deposit: Account is inactive.')
return
}
if amount > 0 {
a.balance += amount
println('Deposited $${amount:.2f}')
}
}
fn (mut a BankAccount) withdraw(amount f64) {
if !a.is_active {
println('Cannot withdraw: Account is inactive.')
return
}
if amount > a.balance {
println('Cannot withdraw: Insufficient funds.')
return
}
if amount > 0 {
a.balance -= amount
println('Withdrew $${amount:.2f}')
}
}
fn main() {
mut acc := BankAccount{
owner: 'Jane Doe'
balance: 150.00
is_active: true
}
acc.display()
acc.deposit(50.50)
acc.withdraw(75.00)
acc.display()
acc.withdraw(200.00)
}EXPECTED OUTPUT (Click to Reveal / Hide)
Account Owner: Jane Doe | Balance: $150.00 | Status: Active
Deposited $50.50
Withdrew $75.00
Account Owner: Jane Doe | Balance: $125.50 | Status: Active
Cannot withdraw: Insufficient funds.Exercise 8: Safe Division with Option/Result
This exercise covers Chapter 8: Error Handling.
Write a function divide(a f64, b f64) !f64 that returns an error when dividing by zero. In main, call this function, handle potential errors cleanly using an or block, and print the results for both a valid division and an invalid division.
SOLUTION (Click to Reveal / Hide)
module main
fn divide(a f64, b f64) !f64 {
if b == 0.0 {
return error('division by zero error')
}
return a / b
}
fn main() {
x := 10.0
y := 2.5
z := 0.0
res1 := divide(x, y) or {
println('Error: ${err}')
return
}
println('${x} / ${y} = ${res1}')
divide(x, z) or {
println('Error occurred: ${err}')
return
}
}EXPECTED OUTPUT (Click to Reveal / Hide)
10 / 2.5 = 4
Error occurred: division by zero errorExercise 9: Modular Math Utility Project
This exercise covers Chapter 9: Organizing Code with Modules.
Describe how to design a modular program containing a main module and a utility sub-module named mathutils. Implement a public function factorial(n int) int inside mathutils and import it into your main module to calculate and print factorial(5).
SOLUTION (Click to Reveal / Hide)
// Directory structure:
// my_project/
// โโโ main.v
// โโโ mathutils/
// โโโ mathutils.v
// mathutils/mathutils.v
module mathutils
pub fn factorial(n int) int {
if n <= 1 {
return 1
}
return n * factorial(n - 1)
}
// main.v
module main
import mathutils
fn main() {
val := 5
result := mathutils.factorial(val)
println('Factorial of ${val} is ${result}')
}EXPECTED OUTPUT (Click to Reveal / Hide)
Factorial of 5 is 120Exercise 10: Unit Testing String Reversal
This exercise covers Chapter 10: Writing Tests in V.
Write a V library containing a public function reverse_string(s string) string. Write a corresponding test file reverse_string_test.v with unit tests verifying correctness for empty strings, single characters, palindromes, and multi-word sentences using assert statements.
SOLUTION (Click to Reveal / Hide)
// reverse.v
module main
pub fn reverse_string(s string) string {
mut runes := s.runes()
mut i := 0
mut j := runes.len - 1
for i < j {
temp := runes[i]
runes[i] = runes[j]
runes[j] = temp
i++
j--
}
return runes.string()
}
// reverse_string_test.v
module main
fn test_reverse_string() {
assert reverse_string('') == ''
assert reverse_string('a') == 'a'
assert reverse_string('radar') == 'radar'
assert reverse_string('hello world') == 'dlrow olleh'
}EXPECTED OUTPUT (Click to Reveal / Hide)
[PASS] test_reverse_stringExercise 11: Concurrent Task Aggregation with Channels
This exercise covers Chapter 11: Concurrency and Channels.
Write a program that spawns three concurrent v-routines. Each v-routine should compute a segment of a calculation (e.g. squaring a number) and send the result back through a shared channel. The main function should receive all three values from the channel, sum them up, and print the total.
SOLUTION (Click to Reveal / Hide)
module main
fn worker(id int, val int, ch chan int) {
println('Worker ${id} starting to calculate square of ${val}')
ch <- (val * val)
}
fn main() {
ch := chan int{cap: 3}
spawn worker(1, 4, ch)
spawn worker(2, 6, ch)
spawn worker(3, 8, ch)
mut sum := 0
for _ in 0 .. 3 {
val := <-ch
sum += val
}
println('Sum of concurrent square results: ${sum}')
}EXPECTED OUTPUT (Click to Reveal / Hide)
Worker 1 starting to calculate square of 4
Worker 2 starting to calculate square of 6
Worker 3 starting to calculate square of 8
Sum of concurrent square results: 116Exercise 12: JSON Parsing and Validation
This exercise covers Chapter 12: Working with Databases and JSON.
Define a struct representing a Task with fields id (int), title (string), and completed (bool). Write a program that takes a JSON string containing an array of tasks, parses it into a V array of Task structs, and prints the titles of the tasks that are not yet completed.
SOLUTION (Click to Reveal / Hide)
module main
import json2
struct Task {
id int
title string
completed bool
}
fn main() {
raw_json := '[
{"id": 1, "title": "Buy groceries", "completed": true},
{"id": 2, "title": "Write V exercise guide", "completed": false},
{"id": 3, "title": "Compile textbook HTML", "completed": false}
]'
tasks := json2.decode[[]Task](raw_json) or {
println('Failed to parse JSON: ${err}')
return
}
println('Pending Tasks:')
for task in tasks {
if !task.completed {
println('- ${task.title}')
}
}
}EXPECTED OUTPUT (Click to Reveal / Hide)
Pending Tasks:
- Write V exercise guide
- Compile textbook HTMLExercise 13: HTTP Client & Query Parameter Parser
This exercise covers Chapter 13: Standard Library & Advanced Features.
Create a V program that imports the net.http and net.urllib modules. Build a small utility that sends a GET request to a public API URL or a dummy server, checks the response status code, and parses query parameters from a URL string, displaying each parameter's key and value.
SOLUTION (Click to Reveal / Hide)
module main
import net.http
import net.urllib
fn main() {
resp := http.get('https://httpbin.org/get') or {
println('Failed to send request: ${err}')
return
}
println('HTTP GET Status Code: ${resp.status_code}')
sample_url := 'https://example.com/search?q=vlang&limit=10&page=2'
parsed_url := urllib.parse(sample_url) or {
println('Failed to parse URL: ${err}')
return
}
params := parsed_url.query()
println('Parsed URL Query Parameters:')
for key, values in params {
println(' ${key}: ${values.join(', ')}')
}
}EXPECTED OUTPUT (Click to Reveal / Hide)
HTTP GET Status Code: 200
Parsed URL Query Parameters:
q: vlang
limit: 10
page: 2Exercise 14: Concurrent Worker Pool for String Transformation
This exercise covers Chapter 14: Useful Boilerplates and Application Templates.
Adapt the worker pool concurrent processing pattern to transform an array of lowercase strings to uppercase concurrently. Use a struct for Job and Result types, spawn multiple worker threads, feed the job channel, close it, and collect the results.
SOLUTION (Click to Reveal / Hide)
module main
struct Job {
id int
data string
}
struct Result {
job_id int
output string
}
fn worker(id int, jobs chan Job, results chan Result) {
for job in jobs {
println('Worker ${id} processing job ${job.id}: "${job.data}"')
results <- Result{
job_id: job.id
output: job.data.to_upper()
}
}
}
fn main() {
num_jobs := 5
num_workers := 3
jobs := chan Job{cap: num_jobs}
results := chan Result{cap: num_jobs}
for i in 1 .. (num_workers + 1) {
spawn worker(i, jobs, results)
}
words := ['apple', 'banana', 'cherry', 'date', 'elderberry']
for i, word in words {
jobs <- Job{
id: i + 1
data: word
}
}
jobs.close()
for _ in 0 .. num_jobs {
res := <-results
println('Result collected: Job ${res.job_id} output = "${res.output}"')
}
}EXPECTED OUTPUT (Click to Reveal / Hide)
Worker 1 processing job 1: "apple"
Worker 2 processing job 2: "banana"
Worker 3 processing job 3: "cherry"
Worker 1 processing job 4: "date"
Worker 2 processing job 5: "elderberry"
Result collected: Job 1 output = "APPLE"
Result collected: Job 2 output = "BANANA"
Result collected: Job 3 output = "CHERRY"
Result collected: Job 4 output = "DATE"
Result collected: Job 5 output = "ELDERBERRY"End of Tutorial
Congratulations! You have completed the comprehensive V Programming tutorial and exercise guide.