# vdoc **Repository Path**: sakana_ctf/vdoc ## Basic Information - **Project Name**: vdoc - **Description**: vlang的个人编写教学文档. - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 4 - **Forks**: 0 - **Created**: 2024-02-23 - **Last Updated**: 2025-07-14 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # vlang语言学习指南 ## 个人简介 **CryingN** * 在第一届VYctf中使用vlang进行命题. * 2023年荣获统信XGodot开发大赛荣誉证书; * 2024年2月开始编写vlang学习指南; * vtf主要开发者, vtf是第一个使用vlang编写的ctf比赛平台; ## 写在前面 这里是不正经的vlang语言学习文档, 我会按自己的学习思路进行编写, 如果想通过该文章进行vlang的学习, 需要自行结合[基本内容](#基本内容)与[关键字](#关键字)两个部分, 本文主要以介绍vlang的使用方法为主, 暂时不推荐以本书作为第一学习语言入手. 介绍一下vlang这门语言: * 我自认为vlang在当前的编程语言竞争中毫无优势; * 我曾考虑过编写一门专用于算法规划的语言, 于是了解到vlang, 据宣传这是一门可以实现自编译的语言(虽然在尝试后感觉和我想象中不太一样) * 在编程界各种语言都有自己的弊端, 在没有选择好方向时很难决定应该学习哪一门语言, vlang成功填补了难以选择的空白, 学习别的语言可能会在以后的工作中没有作用,但是选择vlang在未来的工作中一定没有作用. 以上介绍是我刚开始接触vlang时的一些调侃, 实际上在不断了解后我也会感到, 这门语言还是存在很多可取之处: * vlang是编译到c的二级语言, 开发起来更加容易. * vlang融合了许多golang与rust的语法, 可以称得上是较为前沿的语言, 在熟悉vlang后golang与rust的入门门槛能大幅降低. * vlang的交叉编译能力极为优秀, 若能进行完善, 将能大幅减少开发成本. 衷心希望vlang能在未来能有一个亮眼的表现, 以下为我个人对vlang语言做出的一点微薄贡献. **CryingN** ## 目录 - [个人简介](#个人简介) - [写在前面](#写在前面) - [目录](#目录) - [基本内容](#基本内容) - [下载安装](#下载安装) - [Android](#Android) - [linux](#linux) - [windows](#windows) - [配置编程环境](#配置编程环境) - [vim环境](#vim环境) - [vlang格式](#vlang格式) - [从汇编语言开始](#从汇编语言开始) - [实现第一个解释器](#实现第一个解释器) - [实现网页分析工具内核](#实现网页分析工具内核) - [关于net编程](#关于net编程) - [控制一个简单的数据库](#控制一个简单的数据库) - [关键字](#关键字) - [asm](#asm) ## 基本内容 >- [前置知识](./vlang-doc-zh-master/readme.md) > - [01.基础](./vlang-doc-zh-master/01基础.md) > - [02.类型](./vlang-doc-zh-master/02类型.md) > - [03.基础](./vlang-doc-zh-master/03控制.md) > - [04.结构体](./vlang-doc-zh-master/04结构体.md) > - [05.函数](./vlang-doc-zh-master/05函数.md) > - [06.模块](./vlang-doc-zh-master/06模块.md) > - [07.类型声明](./vlang-doc-zh-master/07类型声明.md) > - [08.并发](./vlang-doc-zh-master/08并发.md) > - [09.其他](./vlang-doc-zh-master/09其他.md) > - [10.进阶](./vlang-doc-zh-master/10进阶.md) > - [11.附录](./vlang-doc-zh-master/11附录.md) > > 感谢[yscsky](https://github.com/yscsky/vlang-doc-zh)对vlang中文部分作出的贡献. ### 下载安装 [vlang](vlang.io)提供了方便的下载渠道, 支持window,linux与macOS三种系统. #### Android 以前测试过vlang似乎不支持在手机中通过Termux执行, 原因暂时不明. #### linux 作者使用archlinux的wsl编译环境,下载Linux的bin文件, 解压存放为v, 例如放在home/admin/v中, 使用软链接将v编译工具存放在bin文件夹中: ```bssh cd / ln -s /home/admin/vlang/v /bin/v ``` 如果测试无问题, 可以在任意位置通过`v`进入vlang环境, 效果如下所示: ```bash [root_cn@archlinux ~]$ v ____ ____ \ \ / / \ \/ / \ / \ / \__/ Welcome to the V REPL (for help with V itself, type exit , then run v help ). Note: the REPL is highly experimental. For best V experience, use a text editor, save your code in a main.v file and execute: v run main.v V [当前版本号] . Use list to see the accumulated program so far. Use Ctrl-C or exit to exit, or help to see other available commands. >>> ``` 如果出现问题, 可对bin文件进行检查, 查看文件链接是否有误: ```bash [root_cn@archlinux ~]$ ls -all /bin/v lrwxrwxrwx 1 root root 30 Aug 22 04:03 /bin/v -> /home/admin/vlang/v ``` > 注意软链接无法对相对位置和软链接位置进行处理. #### windows 在windows中需要配置环境变量并运行**make.bat**文件, 详细配置方法如下: 1. 下载并解压vlang的windows压缩包, 将解压路径添加至环境变量, 使用cmd或powershell(推荐使用powershell, 微软采用powershell更好地替代了cmd)输入vlang对进行查看. ### 配置编程环境 #### vim环境 开发者个人使用vim进行开发, vim是一款linux的主流编辑器,用户可以通过自定义配置出适合自己的vim, 如果大家是第一次使用vim, 只需参考以下配置方式, 这里使用的是archlinux系统, 在查找文件时可能会出现一些区别, archlinux使用pacman包查询是否安装过vim: ```bash sudo pacman -Ss vim ``` 如果还未安装, 可以继续通过pacman进行安装: ```bash sudo pacman -S vim ``` 如果结束出现`(1/1)`表示配置成功, 反之可能是使用源出现问题,可通过以下指令进行更新后再次安装: ```bash sudo pacman -Syyu sudo pacman -S vim ``` 在安装成功后找到`vim`文件夹进行编辑,archlinux使用pacman包安装vim时应该按以下路径进行编辑: ```bash sudo vim /usr/share/vim/vim[版本号]/defaults.vim ``` 进入后在文件末尾添加: ```vim map : call CompileRunGcc() func! CompileRunGcc() exec "w" if &filetype == "v" !v % && time ./%< endif endfunc ``` 完成后可以按`Esc`后通过`:wq`保存退出,不过作为开发者, 个人建议按照以下配置进行添加,在进行其他环境开发中会更加方便: ```vim sudo vim /usr/share/vim/vim91/defaults.vim map : call CompileRunGcc() func! CompileRunGcc() exec "w" if &filetype == "python" !time python % elseif &filetype == "c" !clang % -o %< && time ./%< elseif &filetype == "cpp" !clang++ % -o %< && time ./%< elseif &filetype == "go" !time go run % elseif &filetype == "r" !Rscript % elseif &filetype == "sh" !time bash ./% elseif &filetype == "rust" !rustc % && time ./%< elseif &filetype == "markdown" !vnote % elseif &filetype == "toml" !cargo build elseif &filetype == "cs" !mcs % -out:%<.exe && time mono %<.exe elseif &filetype == "asm" !nasm -f elf64 % -o %<.o && ld %<.o -o %< && time ./%< elseif &filetype == "v" !v % && time ./%< endif endfunc map : call SpecialCompilation() func! SpecialCompilation() exec "w" if &filetype == "rust" !cargo run elseif &filetype == "c" !sdcc % -o %<.ihx && packihx %<.ihx > %<.hex endif endfunc map : call SpecialCompilation() func! SpecialCompilation() exec "w" if &filetype == "c" !time x86_64-w64-mingw32-gcc % -o %<.exe && time ./%<.exe endif endfunc :set number ``` 完成后可以自己尝试以下对v文件进行编译,在使用vim编辑v文件时,只需要按下`F5`,便能跳转到终端界面进行编译. 如果没有弹出有可能是因为计算机的`Fn`键处于关闭状态, 按下`Fn`到亮起状态再进行`F5`编译即可. #### vscode环境 由于个人不常用vscode, 将以最简单的方式vlang的描述配置方式. 1. 下载vlang并配置环境变量, 使vlang可以直接在终端执行 2. 在vscode中找到**扩展**, 找到**V language support for Visual Studio Code**, 安装对应插件. 使用终端直接输入`v [文件名].v`或`v.exe [文件名].v`对文件进行编译, 使用`./[文件名]`运行编译好的文件. ### vlang格式 喵了个咪, 怎么会有这么简单的格式(c/rust患者如是表示),vlang本体可以直接运行, 我们打开vlang,输入`print("hello,world")`,可以得到vlang的第一句编程语言: ```bash [root_cn@archlinux ~]$ v ____ ____ \ \ / / \ \/ / \ / \ / \__/ Welcome to the V REPL (for help with V itself, type exit , then run v help ). Note: the REPL is highly experimental. For best V experience, use a text editor, save your code in a main.v file and execute: v run main.v V [版本号] . Use list to see the accumulated program so far. Use Ctrl-C or exit to exit, or help to see other available commands. >>> println("hello,world") hello,world >>> ``` > 有什么用: > 和python对比过会发现vlang直接运行的效率比python要低上不少, 实际上这很好解释, python是作为一门解释器存在的语言, 可以直接解析代码得到结果; 而vlang是作为一门编译器存在的语言, 需要经历先编译再运行的过程, 两者基本存在于不同赛道. > 虽然vlang几乎没理由直接运行用于处理各种问题, 也许效率还没自己临时新建一个文档来得快(笑), 但在遭遇需要测试的问题时临时新起一个`:memory:`环境还是要方便上不少, 极大地节约了测试时间. 如果配置好了编程环境, 我们可以继续进行下一步编程,新建一个文件,例如`test.v`,输入下面代码: ```go fn main(){ println("hello,world") } ``` 按下`F5`进行编译, 不出意外会打印出hello,world,还有编译运行的时间以供参考. vlang引入了golang的module关键字, 以下为一段比较完整的代码示例: ```go module main import time fn main() { println('hello,vlang!') time.sleep(3) } ``` 以上为一个完整的vlang的基本格式, 后文中的代码可能会根据实际情况进行省略. ### 从汇编语言开始 个人很喜欢vlang设计中的asm接口, 让vlang得以快速进行基本运算, 我们会从asm语言开始, 让入门渗透变得更容易, 先实现我们第一个asm的程序: ```go fn main() { mut a := 10 asm amd64{ mov eax, a add eax, 5 mov a, eax // input "a" ; r (a) } print("a: $(a)") //a: 15 //real 0m0.041s //user 0m0.000s //sys 0m0.007s ``` 正常运行应该能得到结果如上所示, 在上面的程序中我们做了如下的事: 1. 定义了一个可变的变量a. 2. 调用了asm的amd64函数. 3. 将a的值存放在eax寄存器中, 让eax等于eax+5, 然后赋值a为eax. 4. 将变量a输入amd64的函数中. 5. 输出a的结果 以上代码相当于: ```go fn main() { mut a := 10 a += 5 print("a: ${a}") } //a: 15 //real 0m0.035s //user 0m0.000s //sys 0m0.008s ``` **注意:** 以上写法非常危险, 接下来会进行解释 我们考虑这样的情况, 我们和其他人需要在两个不同的部分调用asm语言, 其中我们依旧想要实现`a += 5`, 对方会令`b = ebx`, 如果直接按照以上写法进行编程, 可能会写出代码如下所示: ```go fn main(){ mut a := 10 mut b := 3 asm amd64{ mov eax, a add eax, 5 mov a, eax // input "a" ; r (a) } asm amd64{ mov b, ebx // input "b" ; r (b) } println("a: ${a}") println("b: ${b}") } //a: 8 //b: 870477304 //real 0m0.041s //user 0m0.000s //sys 0m0.007s ``` 因为没有指定寄存器ebx的值, 输出b的值可能为各种奇怪的结果, 但奇怪的是本来我们应该为15的值也改变为8, 这是因为我们没有限制输入输出的结果, 在一次amd64函数中可以有一次输入与一次输出(也有可能是我没理解真正的使用方法), 过程可以调用多次变量, ```go // output a ; = r (a) // input b ; r (b) // quote c,d r (c) r (d) ``` 重写一遍代码: ```go fn main(){ mut a := 10 mut b := 3 c := 5 asm amd64{ mov eax, a add eax, c mov a, eax // input "a" ; r (a) // output "a" ; = r (a) // quote "c" r (c) } asm amd64{ add ebx, 2 mov b, ebx // output "b" ; = r (b) } println("a: ${a}") println("b: ${b}") } //a: 15 //b: 866709962 //real 0m0.112s //user 0m0.000s //sys 0m0.020s ``` 输出结果符合预期. ### 实现第一个解释器 接下来是学习最基本的os库与基本的切片使用, 实现用vlang对计算机进行最基本的编辑. vlang的os库包含很多函数, 编写起来非常严格(相较于python). 开始之前我们先试着实现基础部分的代码: ```go match program[program_counter] { `>` { address++ // address地址增加 } `<` { address-- // address地址减少 } `+` { memory[address]++ // 地址对应记忆增加:wq } `-` { memory[address]-- // 地址对应记忆减少 } `.` { data := memory[address].ascii_str() print(data) // 打印地址值 } `,` { input := os.input_opt('') or { '' } // 读取值并说明错误 memory[address] = input[0] // 忽略换行符 // 字符串以0结尾, 默认为我们将值释放掉 } `[` { stack << program_counter // 循环起始地址添加到调用堆栈 } `]` { if memory[address] != 0 { // 将程序计时器设置为上次循环启动 // 它会导致程序跳转回去重复运行 program_counter = stack[stack.len - 1] } else { // 从堆栈中删除地址并继续 stack.pop() } } else { // 编译器忽略不属于该语言的字符 } } // 递增程序计数器以运行下一条指令 program_counter++ ``` 以上是一个最为简单且抽象的语言:**brainfuck**的实现逻辑, 我们假设存在一段字符串`program`, 将`program_counter`作为字符串的指针, 通过match函数对`program_counter`指向的`program`字符进行判别, 该语言有两个部分组成:`address(地址)`和`memory(记忆)`, 关键字仅有`>`,`<`,`+`,`-`,`.`,`,`,`[`,`]`共8个, 为了避免出现关键字以外的字符, 使用`else{}`对match查找到的非关键词进行忽略, 结尾使用`program__conter++`让其自增1, 等效于`program__conter += 1`, 实现指针跳转到字符串的下一个值, 解释器的核心部分就基本完成了. 我们需要实现的是像**v**文件一样: - 经过编译后生成一个二进制文件`brainfuck`; - 当写好一个brainfuck语言的文件后可以使用`brainfuck`直接进行编译; - `barinfuck`需要判断是否输入了可执行文件名; - `brainfuck`应该判断文件后缀名是否是`.bf`: - 如果是`.bf`正常执行; - 如果不是`.bf`文件需要提示用户; - 执行完以后对结果进行储存: - 存放文件名将后缀从`.bf`修改为`.txt`; - 如果不是`.bf`文件, 需要尝试运行后将文件设为`default.txt`. 读取文件的话需要调用**vlang**的os库, 调用方法类似**python**与**golang**: `import os` 接下来我们用一个小例子让大家熟悉vlang的os库: ```go import os fn main() { mut test_file := os.create('test_file.txt') or { println('文件创建失败') exit(1) } test_file.write_string('test')! test_file.close() println(os.read_file('test_file.txt')!) } // test // real 0m0.037s // user 0m0.007s // sys 0m0.000s ``` 运行结束后应该能够生成文件`test_file.txt`, 并且读取文件内容"test". 这段代码先是在执行目录中创建了`test_file.txt`文件, 如果出现以外, 会输出"文件创建失败"并结束运行, 成功后强制对文件写入'test'字符串, 关闭文件, 然后打印`test_file.txt`文件. >注意: 对文件使用函数操作后如果不使用close()关闭文件, 则文件会在运行结束最后自行保存, 删除`test_file.close()`后不再输出值, 但是文件会储存内容"test". 学会了对文件进行基本使用后还需要对字符串进行编辑, ### 实现网页分析工具内核 本次实现的程序为**blackspider**项目的查询内核, (哦, blackspider还没更新啊, 那没事了.) ### 关于net编程 在此之前我认为应该先介绍一下`spawn`, `spawn`关键字会在函数中创建一个子进程, 子进程随的主进程的结束自动结束: ```go module main import time fn test() { for j in 6 .. 10 { print(j) time.sleep(10) } } fn main() { spawn test() for i in 1 .. 5 { print(i) time.sleep(1) } } // 61728394 ``` 运行结果如上所示, 可以发现test函数与主函数的循环在同时执行. spawn主要应用在服务器上处理多个访问的情况, 我们来试着实现一个简单的服务端: **net**包提供了网络功能, 可以用于侦听端口, 连接TCP/UDP服务进行通信. 以上工具, 以上是一个简单的tcp协议服务端. 首先创建一个**vser.v**文件, 加入以下代码: ```go module main import io import os import net fn main() { if os.args.len < 2 { println('\033[31m[false] \033[0m请添加需要监听的端口,\n 例:vser 12345') exit(1) } port := os.args[1] // 构造一个用于监听ip6的端口 mut server := net.listen_tcp(.ip6, ':' + port)! eprintln('Listen on ${port} ...') // 当监听到外部tcp连接请求时构建一个socket, 创建子进程信息交换. for { mut socket := server.accept()! spawn handle_client(mut socket) } } // 该函数为socket连接中服务器与客户端进行信息交流的通道 fn handle_client(mut socket net.TcpConn) { // defer为退出函数, 关闭socket或汇报错误. defer { socket.close() or { panic(err) } } client_addr := socket.peer_addr() or { return } eprintln('> new client: ${client_addr}') // 通过socket读取客户端发送信息 mut reader := io.new_buffered_reader(reader: socket) // 退出时释放读取的文件 defer { unsafe { reader.free() } } // socket向服务端写入字符 socket.write_string('=== Nice to vServer ===\n') or { return } for { // 对信息进行处理 received_line := reader.read_line() or { return } if received_line == '' { return } println('${client_addr}: ${received_line}') socket.write_string('\033[32m[true] \033[0m已接收: ${received_line}\n') or { return } } } ``` 编译好后执行代码`./vser 12345`, 这将对*12345*端口保持监听, 我们可以使用现成的**gnu-netcat**工具进行测试, 输入`nc 127.0.0.1 12345`, 其中`127.0.0.1`表示回环地址, 直接指向本机地址, `12345`表示访问的端口, 当出现`=== Nice to vServer ===`时表示连接成功, 随便输入一串字符串, Enter后返还`[ture]`, 在服务端显示连接端口发送了字符串, 即表示成功, 以下是演示结果, 服务端: ```bash [root_cn@archlinux chal]$ ./vser 12345 Listen on 12345 ... > new client: [::ffff:127.0.0.1]:50230 [::ffff:127.0.0.1]:50230: hello,world! [::ffff:127.0.0.1]:50230: bye ``` 客户端: ```bash [root_cn@archlinux ser-cli]$ nc 127.0.0.1 12345 === Nice to vServer === hello,world! [true] 已接收: hello,world! bye [true] 已接收: bye ``` 因为是服务器创建了子进程进行连接, 所以当客户端结束后服务端依旧会保持监听状态(这里和`gun-netcat`不太一样, 感兴趣的话可以尝试用`nc -lp 12345`监听端口进行测试). 接下来我们可以继续实现客户端, 新建一个**vcli.v**文件, 输入以下代码: ```go module main import readline {read_line} import os import net fn main() { if os.args.len < 3 { println('\033[31m[false] \033[0m请添加addr与port,\n 例:vcli 127.0.0.1 12345') exit(1) } addr := os.args[1] port := os.args[2] mut socket := net.dial_tcp(addr+':'+port) or { println('\033[31m[false] \033[0m未找到tcp连接') exit(1) } mut data := '' for { // 读取服务器消息 data = socket.read_line() print(data) data = read_line('>') or {''} // 退出设置 if data == 'wq' { data = '正在退出...' for_free(data, mut socket) exit(1) } defer { data = '\n强制退出中...' for_free(data, mut socket) } // 向服务器写入数据 socket.write_string('${data}\n') or { return } } } // 退出函数 fn for_free(data string, mut socket net.TcpConn) { println(data) unsafe{ data.free() } socket.close() or { panic(err) } } ``` 该程序专用于与**vser**进行交互, 编译后依旧让服务端保持监听: `./vser 12345`, 使用**vcli**连接端口: `./vcli 127.0.0.1 12345`, 使用方法参考上面使用`gnu-netcat`, 输入`wq`或使用快捷键: `ctrl`+`C`进行退出. ### 控制一个简单的数据库 sqlite大概是最简单的数据库, 不同于其他数据库, sqlite直接以文件形式将数据库储存于本地, 对于初学者来说可以更加方便地编辑与控制数据库. 在vlang中控制sqlite主要有两种方式: * [直接调用](#直接调用) * [orm映射](#orm映射) 以下会简单介绍两种方式的用法与其优劣, 在构建项目时可根据需要选择sql控制方式. > 在大型工程中使用orm与直接调用sql的编码差别很大, 请根据需要在编写前提前确定好使用哪种方法 #### 环境搭建 vlang本身不包含sqlite环境, 需自行配置, 对于不同用户分别有以下方式进行配置: **Archlinux** `sudo pacman -S sqlite` **Fedora 31** `sudo dnf -y install sqlite-devel` **debian** `sudo apt install sqlite3` **windows** * 下载[sqlite](https://sqlite.org/download.html); * 在**v/thirdparty**中创建**sqlite**文件夹; * 将下载地压缩包解压至**sqlite**文件夹中. #### 直接调用 直接调用的方式简单粗暴, 好处是可以使用完全的sql指令, 但是同时也需要自行防范sql注入的安全性问题. ```go import db.sqlite fn main() { db := sqlite.connect('./data1.db')! db.exec("create table sakana (id integer primary key, name text);") or { panic(err) } db.exec("insert into sakana (name) values ('sudopacman')") or { panic(err) } users := db.exec('select * from sakana') or { panic(err) } println(users) } ``` 运行结束后在目录上可以看到创建了一个**data1.db**文件. #### orm映射 vlang的sql语句映射极大地方便了将代码本地化的流程, 当构建好sql表映射的结构体后不再需要对数据库进行编辑. 但是当前0.4.6版本中orm映射不支持联立查询等复杂操作. ```go import db.sqlite @[table: 'main'] struct Main { id int @[primary; sql: serial] name string } fn main(){ mut db := sqlite.connect('data2.db') or { panic(err) } sql db { create table Main }! data := Main{ name : 'neko' } sql db { insert data into Main }! users := sql db { select from Main }! println(users) } ``` 运行结束后在目录上可以看到创建了一个**data2.db**文件. ## 关键字 |关键词|解释|使用| |:--:|:--|:--| |mut|定义变量为可变变量, 在未使用`mut`对变量进行声明时变量不可变|mut [变量名] := [变量值]| |asm|汇编语言编写接口, 可通过指定`asm amd64(x86系统)`进行调用|asm (amd64) { [asm语言] }| |[eax, ebx, ecx, edx, esi, edi, ebp, esp......]()|X86汇编语言中的寄存器, 即相当于高级语言中的变量|| |[mov, add, ...](####asm)|汇编语言指令|mov [变量1], [变量2]| |match|用于遍历搜索对应值|match [数值 or 字符串]{[判别字符1] {[判别结果1]} [判别字符2] {[判别结果2]} ... else {[非预期判别结果]} }| |import|用于调用第三方库|import [库名]| |os.create()|创建文件的函数,需要调用os库进行使用|mut [变量名 File] := os.create([文件路径 str]) or { [非预期结果] exit(1) }| |write_string()|对文件写入字符串|[变量名 File].write_string([写入内容] str) or { [非预期结果] }| |close()|对编辑好的文件进行关闭及保存|[变量名 File].close()| |os.read_file()|对文件进行读取|os.read_file([文件路劲 str]) or { [非预期结果] }| |spawn|用于创建一个子进程|spawn print('test')| |defer|退出模块前最后执行的内容|defer { end_function() }| ### asm | 关键词 | 解释 | | :----: | :--- | | mov | 赋值 | | add | 加法 |