数组与切片
需要预先了解: 运算符与流程控制 数组与切片 数组和切片是 Go 里最常用的两种序列容器,但它们的语义差别很大:数组是值类型,赋值和传参会整份拷贝;切片是一个包含"指针 + 长度 + 容量"的表头,拷贝表头后两个切片仍指向同一块底层数组。理解切片的表头结构,是理解后面所有"共享底层数组"陷阱的前提。 数组 数组的声明方式是 [长度]元素类型: var a [3]int // 零值数组:[0 0 0] b := [3]int{1, 2, 3} // 带初值 c := [...]int{1, 2, 3, 4} // 用 ... 让编译器数长度 d := [5]int{0: 1, 4: 5} // 指定下标初始化:[1 0 0 0 5] 长度是类型的一部分 [3]int 和 [5]int 是两个不同的类型,不能互相赋值: var x [3]int = [5]int{} // 编译错误 cannot use [5]...
片元着色
需要预先学习: 算了别看了,前面的都得学 片元着色 我们在 光栅化 阶段得到了 片元,片元本身会携带顶点经过插值后的属性,但是这些属性距离最终显示的像素颜色还有一段距离。片元着色器(fragment shader)就是在这个阶段对片元进行处理,计算出最终的结果(与 顶点着色器 一样,我们需要的结果并不一定是颜色)。 也有人将片元着色器(fragment shader)称为像素着色器(pixel shader),但是严格来说片元与像素并不等价(详见 片元)。笔者认为"片元着色器"这个叫法更为恰当,因此本博客统一使用"片元着色器"。 前面的几篇处理的对象都不太一样:变换、投影处理的是顶点的坐标,图元装配与裁剪处理的是三角形本身,光栅化处理的是"哪些像素被覆盖"。到了片元着色,处理的对象变成每个被覆盖像素上的一个片元,它不再改变几何的形状,也不改变覆盖关系,只负责给这个片元算出一组值。 那么,片元着色的核心问题有两个:每个片元要算出什么值,以及这些值最终...
map 与字符串
需要预先了解: 数组与切片 map 与字符串 map 是 Go 内置的键值容器,string 是 UTF-8 编码的只读字节序列。两者都是使用频率极高的类型,也各有几个容易踩坑的地方:nil map 不能写入、map 的遍历顺序是随机的、字符串的 len 返回的是字节数而不是字符数。 map 的基本操作 声明与初始化 var m map[string]int // 声明,零值是 nil,不能直接写入 m = make(map[string]int) // 用 make 初始化 m2 := map[string]int{"a": 1, "b": 2} // 字面量 m3 := make(map[string]int, 16) // 预分配容量(提示,不是硬上限) 增、删、改、查 m := map[string]int{"a": 1, "b": 2} m["c"] = 3 // 增 m["a"] = 100 // 改 delete(m, "b") // 删 fmt.Println(m, l...
内存基础
进制 在基数为 R 的进制中,包含 R 个数字符号, 分别是 0, 1, 2, …, R-1 一个整数n位、小数m位的R进制数字,可以表示为: (N)R=(Kn−1Kn−2...K0.K−1K−2...K−m)R(N)_{R} = (K_{n-1} K_{n-2} ... K_0.K_{-1}K_{-2}...K_{-m})_{R} (N)R=(Kn−1Kn−2...K0.K−1K−2...K−m)R 其中,KiK_iKi 是第 iii 位上的数字符号,满足 0≤Ki<R0 \leq K_i < R0≤Ki<R。 同时,也可以用连加符号来表示: (N)R=∑i=−mn−1KiRi(N)_{R} = \sum_{i=-m}^{n-1} K_i R^i (N)R=i=−m∑n−1KiRi 二进制 二进制是基数为 2 的进制系统,使用两个数字符号:0 和 1。每一位上的权重是 2 的幂次方。 如: (1101)2=1∗23+1∗22+0∗21+1∗20=8+4+0+1=(13)10(1101)_2 = 1 * 2^3 + 1 * 2^2 ...
函数
需要预先了解: map 与字符串 函数 Go 的函数有两个显著特点:参数和返回值的类型都写在名字后面,以及支持多返回值。除此之外,函数在 Go 里是一等公民——可以赋值给变量、作为参数传递、作为返回值,这也让闭包成为日常写法。这篇还会讲清楚一个容易被忽略的事实:Go 的参数传递永远是值拷贝。 定义与调用 func add(a int, b int) int { return a + b } func 是关键字; add 是函数名; 参数列表里,名字在前、类型在后;相邻参数类型相同时可以合并写成 a, b int; 返回值类型写在参数列表之后。 没有返回值时省略返回类型: func greet(name string) { fmt.Println("你好,", name) } 多返回值 函数可以返回多个值,这是 Go 里处理"结果 + 错误"的标准形式: func divmod(a, b int) (int, int) { return a / b, a % b } q, r...
结构体与指针
需要预先了解: 函数 结构体与指针 **结构体(struct)**是把一组字段打包成一个整体,指针用来在函数之间共享同一个对象。Go 的指针比大多数语言的指针"干净":不能做算术运算,不能随意转换类型。这一篇把两者的用法和几个反直觉的规则讲清楚。 指针 & 取地址,* 解引用: a := 10 p := &a // p 的类型是 *int fmt.Println(a, p, *p) // 10 0x104d44a300e0 10 *p = 20 // 通过指针修改 a fmt.Println(a) // 20 指针的零值是 nil: var np *int fmt.Println("np == nil:", np == nil) // true 对 nil 指针解引用会 panic(invalid memory address or nil pointer dereference),使用前要判空。 没有指针算术 指针不能加减、不能自增: p++ invalid operation: p++...
方法
需要预先了解: 结构体与指针 方法 **方法(method)**是带接收者的函数。接收者写在 func 和函数名之间,把方法和某个类型绑定起来。方法看起来只是多了一个参数,但"值接收者还是指针接收者"这个选择会直接影响方法能否修改对象,以及该类型能否满足某个接口。 定义 type Counter struct { n int } func (c Counter) Get() int { return c.n } (c Counter) 就是接收者。调用时用点号: c := Counter{n: 5} fmt.Println(c.Get()) // 5 方法名不能与字段名冲突。 值接收者 vs 指针接收者 值接收者拿到的是调用者的副本,修改副本不影响原对象;指针接收者拿到的是地址,可以修改原对象: func (c Counter) IncrVal() { c.n++ } // 改副本 func (c *Counter) IncrPtr() { c....
接口
需要预先了解: 方法 接口 **接口(interface)**只描述"能做什么",不关心"是什么"。调用方依赖接口,具体实现可以是任意类型——这就是把"变化的实现"和"稳定的调用逻辑"隔离开的方式。Go 的接口有两个特点:实现是隐式的,不需要显式声明;接口值本身由"动态类型 + 动态值"两部分组成,后者是理解 nil 接口陷阱的关键。 从需求引入 假设要打印一批图形的面积。如果函数参数写成具体类型,每加一种图形就要改一次函数签名: func printArea(r Rect) { /* ... */ } func printArea2(c Circle) { /* ... */ } 调用方其实只关心"这个对象能不能算出面积"。把这件事抽象出来: type Shape interface { Area() float64 Name() string } 任何拥有这两个方法的类型都自...
标识符
标识符 标识符(identifier):在程序中用来充当变量、函数、类等的名字。 标识符的命名规则 1.标识符只能由字母、数字和下划线组成,并且必须以字母或下划线开头。 int myVariable = 10; // 正确,myVariable 以字母开头 int _myVariable = 20; // 正确,_myVariable 以下划线开头 int myVariable2 = 40; // 正确,可以包含数字 int 2myVariable = 30; // 错误,可以包含数字但不能以数字开头 int my-Variable = 40; // 错误,标识符中不能包含其它字符 2.标识符不能是 C++ 的关键字。 这里 C++ 的关键字不需要特殊记忆,因为在编程过程中会经常接触到它们,自然而然就会记住它们了。 int int = 10; // 错误,int 是 C++ 的关键字 int while = 20; // 错误,while 是 C++ 的关键字 int...
错误处理
需要预先了解: 接口 函数 错误处理 Go 把"可预期的失败"当作普通的返回值处理,而不是语言级的异常机制。这套设计的核心是 error 接口加 if err != nil 惯例。与之相对,panic 只用于"程序不该继续运行"的情况。这一篇把 error 的包装与解包、defer 的完整机制、panic/recover 的边界都讲清楚。 error 接口 error 是标准库里的一个接口,只有一个方法: type error interface { Error() string } 创建错误有两种常用方式: err1 := errors.New("资源不存在") err2 := fmt.Errorf("打开文件 %s 失败", "a.txt") errors.New 返回一个固定文本的错误;fmt.Errorf 可以格式化文本。两者都满足 error 接口。 if err != nil 惯例 Go 的错误处理惯例是"检查并提前返回",而不是层层嵌套: func find(i...



