V语言教程

V语言是一个简单、快速、安全的编译型语言,比较适合于开发可维护的软件。

Google讨论组:

简介

V语言是一种静态类型的编译语言,用于构建可维护的软件。它与Go类似,同时也受到Oberon,Rust,Swift等语言设计的影响。

V语言也是一种非常简单的语言。通读本教程只要半个小时,你就可以掌握语言的全部特性。

尽管语言简单,但是为开发人员提供了强大的特性。任何其它语言可以实现的功能,V语言都可以实现。

Hello World

fn main() {
	println('hello https://vlang-zh.cn')
}

函数用fn关键字定义或声明。返回类型在函数名称后面。在这个例子中main函数没有返回值,因此返回值类型被忽略了。

和C语言一样,main函数是程序的入口函数。println是内置函数之一,它打印到标准输出。

在一个单一文件的V程序中,main函数可以被忽略。这对于学习语言的一些小代码片段很友好。为了演示,后面的例子就忽略了main函数。

因此“Hello World”程序可以写的再简单一点:

println('hello https://vlang-zh.cn')

注释

// 单行注释
/* 多行注释.
   /* 支持嵌套注释. */
*/

函数

fn main() {
	println(add(77, 33))
	println(sub(100, 50))
}

fn add(x int, y int) int {
	return x + y
}

fn sub(x, y int) int {
	return x - y
}

同样,类型在参数名称之后。

同样,和Go语言、C语言一样,函数不能重载。因为这样可以提高代码等可维护性和可读性。

函数可以在声明之前就使用:虽然

函数可以在声明之前使用:虽然add和sub在main之后声明,但是在main中就可使用。V语言中所有的声明都是可以提前使用,因此不用关心声明的顺序。

变量

name := 'Bob'
age := 20
large_number := i64(9999999999)
println(name)
println(age)
println(large_number) 

变量使用:=声明和初始化,这是V语言唯一定义变量的方式。因此,V语言中所有的变量必须指定一个初始化的值。

变量的类型是从右值推导来的。要使用其它类型,必须手工强制转换类型:使用T(v)表达式将v转换为T类型。

和其它主流语言不同的是,V语言只能在函数内部定义变量。V语言没有模块级别的全局变量,因此也没有全局状态。

mut age := 20
println(age)
age = 21
println(age)

使用=给变量重新赋值。不过在V语言中,变量默认是不可再次改变的。如果需要再次改变变量的值,必须用mut修饰变量。可以尝试删除mut,然后再编译上面的代码。

需要注意:==的差异,前者是用于声明和初始化变量,后者是重新给变量赋值。

fn main() {
	age = 21
}

上面的代码将不能编译,因为变量没有被声明过。V语言中所有的变量必须要先声明。

fn main() {
	age := 21
}

上面的代码依然不能被编译,因为V语言中禁止声明没有被使用的变量。

fn main() {
	a := 10 
	if true {
		a := 20
	} 
}

和很多其它语言不同的是,不同块作用域的变量不得重名。上面的例子中,变量a已经在外层被声明过,因此不能再声明a名字的变量。

基础类型

bool

string

i8  i16  i32  i64      i128 (soon)
u8  u16  u32  u64      u128 (soon) 

byte // alias for u8  
int  // alias for i32  
rune // alias for i32, represents a Unicode code point  

f32 f64

byteptr
voidptr

和C语言、Go语言不同的是,int始终是32bit大小。

字符串

name := 'Bob' 
println('Hello, $name!')  // `$` is used for string interpolation 
println(name.len) 

bobby := name + 'by' // + is used to concatenate strings 
println(bobby) // ==> "Bobby"  

println(bobby.substr(1, 3)) // ==> "ob"  
// println(bobby[1:3]) // This syntax will most likely replace the substr() method   

V语言中,字符串是一个只读的字节数组。字符串数据采用UTF8编码。

单引号和双引号都可以用户包含字符串面值(TODO:双引号目前还不支持)。为保持一致性,vfmt会将双引号字符串转换为单引号,除非该字符串包含单引号字符。

因为字符串是只读的,因此字符串的取子字符串的操作会比较高效:不需要复制,也不需要额外分配内存。

V语言中运算符两边值的类型必须是一样的。比如下面的代码,如果age是int类型的话,是不能正确编译的:

println('age = ' + age)

我们需要将age转换为string类型:

println('age = ' + age.str())

或者在字符串内部直接嵌入表达式(这是比较完美的方式):

println('age = $age')

数组

nums := [1, 2, 3]
println(nums)
println(nums[1]) // ==> "2" 

mut names := ['John']
names << 'Peter' 
names << 'Sam' 
// names << 10  <-- This will not compile. `names` is an array of strings. 
println(names.len) // ==> "3" 
println('Alex' in names) // ==> "false" 

// We can also preallocate a certain amount of elements. 
nr_ids := 50
mut ids := [0 ; nr_ids] // This creates an array with 50 zeroes 

数组的第一个元素决定来数组的类型,比如[1, 2, 3]对应整数类型的数组[]int。而['a', 'b']对应字符串数组[]string

数组中的每个元素必须有相同的类型,比如[1, 'a']将不能编译。

<<运算符用于向数组的末尾添加元素。

而数组的.len成员返回数组元素的个数。这是一个只读的属性,用户不能修改。V语言中所有导出的成员默认都是只读的。

val in array表达式判断val值是否是在数组中。

Maps

mut m := map[string]int{} // Only maps with string keys are allowed for now  
m['one'] = 1
println(m['one']) // ==> "1"  
println(m['bad_key']) // ==> "0"  
// TODO: implement a way to check if the key exists 

numbers := { // TODO: this syntax is not implemented yet  
	'one': 1,
	'two': 2,
}

If

a := 10
b := 20
if a < b {
	println('$a < $b')
} else if a > b {
	println('$a > $b')
} else {
	println('$a == $b')
}

if语句和大多数编程语言类似。和C语言不同的是,条件部分不需要小括弧,而大括弧是必须的。

if同时也可以当作表达式使用:

num := 777
s := if num % 2 == 0 {
	'even'
}
else {
	'odd'
}
println(s) // ==> "odd"

in运算符

in运算符判断数组是否包含某个元素。

nums := [1, 2, 3]
println(1 in nums) // ==> true 

对于需多个值之一的相等判断比较简洁:

if parser.token == .plus || parser.token == .minus || parser.token == .div || parser.token == .mult {
	... 
} 

if parser.token in [.plus, .minus, .div, .mult] {
	... 
}

V语言会优化上述的表达式,因此两种方式产生的目标代码都是差不多的。

for循环

V语言只有for一种循环结构。

numbers := [1, 2, 3, 4, 5]
for num in numbers {
	println(num)
}
names := ['Sam', 'Peter']
for i, name in names {
	println('$i) $name')  // Output: 0) Sam
}

其中for .. in循环用于迭代遍历数组中每个元素的值。如果同时还需要元素对应的索引的话,可以用for index, value in语法。

mut sum := 0
mut i := 0
for i <= 100 {
	sum += i
	i++
}
println(sum) // ==> "5050"

这种风格的循环和其它语言中的while循环类似。当循环条件为false的时候结束循环迭代。

同样,循环条件不需要小括弧,而大括弧又是必须的。

mut num := 0
for {
	num++
	if num >= 10 {
		break 
	} 
}
println(num) // ==> "10"

循环的条件可以省略,省略后类似一个无限循环。

for i := 0; i < 10; i++ {
	println(i)
}

最后是C语言风格的for循环。这种方式的循环比while循环更安全,因为while循环很容易忘记更新循环的计数器。

这里的i不需要用mut声明,因为这里的变量默认是可变的。

switch多分支

os := 'windows' 
print('V is running on ')
switch os {
case 'darwin':
	println('macOS.')
case 'linux':
	println('Linux.')
default:
	println(os) 
}
// TODO: replace with match expressions 

switch对应多个if - else分支的简化。当遇到相等的第一个case对应的语句执行相应的语句。和C语言不同的是,不需要在每个case写break。

结构体

struct Point {
	x int
	y int 
} 

p := Point{
	x: 10 
	y: 20 
} 
println(p.x) // Struct fields are accessed using a dot 

上面的结构体都在栈上分配。如果需要在堆上分布,需要用取地址的&操作符:

pointer := &Point{10, 10}  // Alternative initialization syntax for structs with 3 fields or fewer
println(pointer.x) // Pointers have the same syntax for accessing fields

V语言不支持子类继承,但是可以嵌入匿名结构体成员:

// TODO: this will be implemented later in June
struct Button {
	Widget
	title string
}

button := new_button('Click me')
button.set_pos(x, y)

// Without embedding we'd have to do
button.widget.set_pos(x,y)

结构体成员访问修饰符

结构体成员默认是私有并且不可修改的(结构体模式是只读)。但是可以通过pub设置为公开的,通过mut设置为可写的。总体来说有以下五种组合类型:

struct Foo {
	a int     // private immutable (default) 
mut: 
	b int     // private mutable 
	c int     // (you can list multiple fields with the same access modifier)   
pub: 
	d int     // public immmutable (readonly) 
pub mut: 
	e int     // public, but mutable only in parent module  
pub mut mut: 
	f int 	  // public and mutable both inside and outside parent module  
}                 // (not recommended to use, that's why it's so verbose) 

例如在builtin模块定义的字符串类型:

struct string {
	str byteptr
pub:
	len int
}

可以看出字符串是一个只读类型。

字符串结构体中的byte指针在builtin模块之外不可访问。而len成员是模块外部可见的,但是外部是只读的。

fn main() {
	str := 'hello'
	len := str.len // OK
	str.len++      // Compilation error
}

方法

struct User {
	age int 
} 

fn (u User) can_register() bool {
	return u.age > 16 
} 

user := User{age: 10} 
println(user.can_register()) // ==> "false"  

user2 := User{age: 20} 
println(user2.can_register()) // ==> "true"  

V语言没有类,但是可以基于类型定义方法。

方法是一种带有接收者参数的特殊函数。

接收者参数出现在fn关键字和方法名字之间,方法名之后也可以有普通的参数。

在上面的例子中,can_register方法有一个User类型的接收者参数u。V语言的习惯是不要用self或this这类名字作为接收者参数名,而是使用短小有意义的名字。

默认都是纯函数

V语言的函数默认是纯函数,也就是函数的输出结果只依赖输入的参数,并且没有其它的副作用。

因为V语言没有全局变量,并且所有的参数默认都是只读的,即使传入的引用也是默认只读的。

然后V语言并不纯的函数式语言。我们可以通过mut关键字让函数参数变得可以被修改:

struct User {
mut:
	is_registered bool 
} 

fn (u mut User) register() {
	u.is_registered = true 
} 

mut user := User{} 
println(user.is_registered) // ==> "false"  
user.register() 
println(user.is_registered) // ==> "true"  

在这个例子中,接收者参数u用mut关键字标注为可变的,因此方法内部可以修改user状态。mut也可以用于其它的普通参数:

fn multiply_by_2(arr mut []int) {
	for i := 0; i < arr.len; i++ {
		arr[i] *= 2
	}
}

mut nums := [1, 2, 3]
multiply_by_2(mut nums)
println(nums) // ==> "[2, 4, 6]"

注意,调用函数的时候也必须给nums增加mut关键字。这样可以清楚表达被调用的函数可能要修改这个值。

最好是通过返回值返回结果,而不是修改输入的函数参数。修改参数尽量控制在程序性能比较关键的部分,这样可以即使那分配和复制的开销。

使用user.register()user = register(user)代替 register(mut user)

V语言可以用简洁的语法返回修改的对象:

fn register(u User) User { 
	return { u | is_registered: true } 
}

user = register(user) 

常量

const (
	PI    = 3.14
	World = 'https://vlang-zh.cn'
) 

println(PI)
println(World)

常量通过const关键字定义,只能在模块级别定义常量,不能在函数内部定义常量。

常量名必须大写字母开头。这样有助于区别常量和变量。

常量值永远不会被改变。

V语言的常量支持多种类型,甚至是复杂类型的值:

struct Color {
    r int
    g int
    b int
}

fn (c Color) str() string { return '{$c.r, $c.g, $c.b}' }

fn rgb(r, g, b int) Color { return Color{r: r, g: g, b: b} }

const (
    Numbers = [1, 2, 3]

    Red  = Color{r: 255, g: 0, b: 0}
    Blue = rgb(0, 0, 255)
)

println(Numbers)
println(Red)
println(Blue)

因为不支持全局的变量,所以支持全局的复杂类型的常量就变得很有必要。

模块

V是一个模块化的语言。它鼓励创建可复用的模块,而且创建模块也很简单。要创建模块需要先创建一个同名的目录,然后里面包含.v后缀名的文件:

cd ~/code/modules
mkdir mymodule
vim mymodule/mymodule.v
// mymodule.v
module mymodule

// To export a function we have to use `pub`
pub fn say_hi() {
	println('hello from https://vlang-zh.cn!')
}

mymodule目录下可以有多个v源代码文件。

然后通过v -lib ~/code/modules/mymodule命令编译模块。

然后就可以在自己的代码中使用了:

module main

import mymodule

fn main() {
	mymodule.say_hi()
}

每次调用模块中的函数必须在函数前面指定模块名。这虽然有点冗长,但是代码更容易阅读和为何,我们一眼就可以看出函数是属于那个模块的。在大型代码库中这很重要。

模块名要短小,一般不要超出10个字符。而且模块也不能出现循环依赖。

所以的模块都将静态编译到单一的可执行程序中。

接口

struct Dog {}
struct Cat {}

fn (d Dog) speak() string { 
	return 'woof'
} 

fn (c Cat) speak() string { 
	return 'meow' 
} 

interface Speaker {
	speak() string
}

fn perform(s Speaker) { 
	println(s.speak())
} 

dog := Dog{} 
cat := Cat{} 
perform(dog) // ==> "woof" 
perform(cat) // ==> "meow" 

类型通过实现的方法满足接口。和Go语言一样,V语言也是隐式接口,类型不需要显式实现接口。

枚举

enum Color {
	red green blue 
} 

mut color := Color.red
// V knows that `color` is a `Color`. No need to use `Color.green` here.
color = .green 
println(color) // ==> "1"  TODO: print "green"? 

可选类型和错误处理

struct User {
	id int 
	name string
} 

struct Repo {
	users []User 
} 

fn new_repo() Repo {
	return Repo {
		users: [User{1, 'Andrew'}, User {2, 'Bob'}, User {10, 'Charles'}]
	}
} 

fn (r Repo) find_user_by_id(id int) ?User { 
	for user in r.users {
		if user.id == id {
			// V automatically wraps this into an option type  
			return user 
		} 
	} 
	return error('User $id not found') 
} 

fn main() {
	repo := new_repo() 
	user := repo.find_user_by_id(10) or { // Option types must be handled by `or` blocks  
		return  // `or` block must end with `return`, `break`, or `continue`  
	} 
	println(user.id) // ==> "10"  
	println(user.name) // ==> 'Charles'
}

V语言针对函数返回值增加了一个可选的属性,这样可以用于处理失败的情况。

将函数升级到可选类型的返回值很简单,只需要给返回值类型增加一个?就可以,这样就可以区别错误和真正的返回值。

如果不需要返回错误信息,可以简单返回Node(TODO:还没有实现)。

这是V语言处理错误的主要手段。函数的返回值依然是值,但是错误处理要简洁很多。

当然,错误还可以继续传播:

resp := http.get(url)?
println(resp.body)

http.get返回的是?http.Response可选类型。如果错误发生,将传播到调用函数,这里是导致main函数抛出异常。

上面代码是下面代码的简写:

resp := http.get(url) or {
	panic(err)
}
println(resp.body)

七月的泛型

struct Repo⟨T⟩ {
	db DB
}

fn new_repo⟨T⟩(db DB) Repo⟨T⟩ {
	return Repo⟨T⟩{db: db}
}

// This is a generic function. V will generate it for every type it's used with. 
fn (r Repo⟨T⟩) find_by_id(id int) ?T {  
	table_name := T.name // in this example getting the name of the type gives us the table name 
	return r.db.query_one⟨T⟩('select * from $table_name where id = ?', id)
}

db := new_db()
users_repo := new_repo⟨User⟩(db)
posts_repo := new_repo⟨Post⟩(db)
user := users_repo.find_by_id(1)? 
post := posts_repo.find_by_id(1)? 

为了方便阅读,允许使用⟨⟩代替<>。vfmt最终会将⟨⟩替换为<>

并发

并发模型和Go语言类似。通过go foo()来并发执行foo()函数调用。目录每个并发函数运行在独立的系统线程。稍后我们会实现和goroutine类似的调度器。

JSON解码

struct User {
	name string
	age  int 
	foo  Foo    [skip]  // Use `skip` attribute to skip certain fields 
} 

data := '{ "name": "Frodo", "age": 25 }'
user := json.decode(User, data) or {
	eprintln('Failed to decode json')
	return 
} 
println(user.name)
println(user.age) 

JSON是目前流行的格式,因此V语言内置了JSON的支持。

json.decode解码函数的第一个参数表示要解码的类型,第二个参数是JSON字符串。

V语言会重新生成JSON的编码和解码的代码。因为不使用运行时的反射机制,因此编码和解码的速度都非常快。

单元测试

// hello.v 
fn hello() string {
	return 'Hello world'
} 

// hello_test.v 
fn test_hello() {
    assert hello() == 'Hello world'
}

所有测试函数都必须放在*_test.v文件中,测试函数以test_开头。通过v hello_test.v运行单个测试代码,通过v test mymodule测试整个模块。

内存管理

V语言没有自动内存回收(GC)和引用计数。V语言会在编译阶段完成必要的清理工作。例如:

fn draw_text(s string, x, y int) {
	...
}

fn draw_scene() {
	... 
	draw_text('hello $name1', 10, 10)
	draw_text('hello $name2', 100, 10)
	draw_text(strings.repeat('X', 10000), 10, 50)
	... 
}

因为字符串没有从draw_text函数逃逸,因此函数调用返回之后就可以被清理。实际上这几个函数调用不会产生任何内存分配的行为。因为两个字符串比较小,V语言会使用提前准备好的缓冲区构造字符串。

对于复杂的情况,目前还需要手工管理内存。但是我们将很快解决这个问题。

V语言运行时会检测内存泄露并报告结果。要释放数组,可以使用free()方法:

numbers := [0; 1000000] 
...
numbers.free()

Defer

有待实现。

vfmt

有待实现。


附录A:关键字

V语言有22个关键字(比Go语言的25个关键字还少):

break 
const  
continue 
defer 
else 
enum 
fn
for
go
goto
if
import
in 
interface 
match 
module 
mut 
or 
pub 
return
struct
type 

附录B:运算符

+    sum                    integers, floats, strings
-    difference             integers, floats 
*    product                integers, floats 
/    quotient               integers, floats 
%    remainder              integers

&    bitwise AND            integers
|    bitwise OR             integers
^    bitwise XOR            integers

<<   left shift             integer << unsigned integer
>>   right shift            integer >> unsigned integer 


Precedence    Operator
    5             *  /  %  <<  >>  & 
    4             +  -  |  ^
    3             ==  !=  <  <=  >  >=
    2             &&
    1             || 


Assignment Operators 
+=   -=   *=   /=   %=
&=   |=   ^=
>>=  <<= 

关注光谷码农公众号,了解V语言更多资讯: