指针 (Pointers)

Go 保留了指针的核心能力(间接寻址、传地址),但砍掉了指针算术和引用类型 &T,换来内存安全和 GC 兼容。 C++ 对照速查表 概念 C++ Go 差异说明 声明指针 int* p / int *p var p *int Go 把 * 放在类型前 取地址 &x &x 完全相同 解引用 *p *p 完全相同 指针算术 p++, p + n 不支持 Go 禁止,用切片代替 空指针 NULL / nullptr nil Go 没有 NULL 关键字 引用类型 int& ref = x 不存在 Go 没有引用,统一用指针 new new int (调用构造) new(int) → *int Go 的 new 只分配零值内存,不调用构造函数 struct 访问 ptr->field ptr.field Go 编译器自动解引用,不需要 -> make 无对应物 make([]int, 5) 仅用于 slice/map/channel,返回初始化后的值(不是指针) 1. 指针基础:&、*、*T package main import "fmt" func main() { x := 42 var p *int = &x // p 是指向 x 的指针,*int 表示"指向 int 的指针" fmt.Println("x 的值:", *p) // 解引用:通过指针读取值 → 42 fmt.Println("x 的地址:", p) // 打印地址 → 0xc0000b4008(类似) *p = 100 // 通过指针修改原变量 fmt.Println("修改后 x:", x) // → 100 // 零值:指针的零值是 nil var q *int fmt.Println("q == nil:", q == nil) // → true } 要点:*int 的 * 是类型修饰符(“int 的指针”),*p 的 * 是操作符(“解引用”)。 ...

2026 年 10 月 13 日

字符串与 Rune

Go 字符串的本质、Unicode 处理与常用操作速查 一、简述 Go 的 string 是不可变的字节序列,底层以 UTF-8 编码存储。它是一个值类型(赋值和传参都会拷贝),长度固定后不能修改。 与 C++ std::string 最大的区别: 不可变——没有 operator[] 返回引用,不能就地修改字符 UTF-8 原生支持——但 len() 返回的是字节数,不是字符数 rune 类型——用于表示单个 Unicode 码点(int32 别名) 二、C++ 对照速查表 Go C++ 说明 string std::string Go 是不可变值类型;C++ 可变 rune char32_t rune = int32,表示一个 Unicode 码点 byte uint8 / unsigned char 一个字节 len(s) s.size() Go 返回字节数,不是字符数 s[i] s[i] Go 返回 byte;C++ 返回 char&(可写) for _, r := range s for (char32_t c : s) Go 遍历 rune;C++ 依赖编码 strings.Builder std::ostringstream 高效字符串拼接 strconv.Itoa(n) std::to_string(n) 整数转字符串 strconv.Atoi(s) std::stoi(s) 字符串转整数 strings.Contains std::string::find 判断子串是否存在 []byte(s) reinterpret_cast 显式转换(会拷贝数据) 没有隐式转换 std::string 构造 string 和 []byte 必须显式转换 三、代码示例 3.1 string 底层:不可变字节序列 package main import "fmt" func main() { s := "Hello, 世界" fmt.Println("len:", len(s)) // 13(字节数!不是字符数) // s[0] = 'h' // 编译错误!字符串不可变 // 底层就是字节数组的只读视图 b := []byte(s) // 转为 []byte 后可修改 b[0] = 'h' s2 := string(b) // 再转回 string fmt.Println(s2) // hello, 世界 } 3.2 rune 类型与 byte vs rune package main import "fmt" func main() { s := "Go语言" // byte —— 字节视角 fmt.Println("字节数:", len(s)) // 8(G=1, o=1, 语=3, 言=3) // rune —— 字符视角 runes := []rune(s) fmt.Println("字符数:", len(runes)) // 4 // 每个 rune 是一个 int32,代表一个 Unicode 码点 fmt.Printf("语 的 Unicode: U+%04X\n", runes[2]) // U+8BED } 3.3 遍历字符串:range vs 索引 package main import "fmt" func main() { s := "Hello世界" // 方式一:for range —— 按 rune 遍历(推荐用于字符级操作) fmt.Println("=== for range ===") for i, r := range s { fmt.Printf("字节索引 %d, rune: %c (U+%04X)\n", i, r, r) } // 输出:H(0), e(1), l(2), l(3), o(4), 世(5), 界(8) // 注意:i 是字节索引!"世"从第5字节开始,占3字节,"界"从第8字节开始 // 方式二:for i —— 按字节遍历 fmt.Println("=== for i (字节) ===") for i := 0; i < len(s); i++ { fmt.Printf("s[%d] = 0x%02X\n", i, s[i]) } // 中文字符的字节是 0xE4 0xB8 0xAD 这样的三字节序列 // 方式三:转为 []rune 后按索引遍历 r := []rune(s) for i := 0; i < len(r); i++ { fmt.Printf("r[%d] = %c\n", i, r[i]) } } 3.4 utf8 包 package main import ( "fmt" "unicode/utf8" ) func main() { s := "Go语言" // 获取 rune 数量(等价于 len([]rune(s)) 但更高效) fmt.Println("字符数:", utf8.RuneCountInString(s)) // 4 // 验证是否为合法 UTF-8 fmt.Println("合法 UTF-8:", utf8.ValidString(s)) // true // 非法 UTF-8 字节 bad := []byte{0xCE, 0xBA, 0xE1, 0xBD, 0xB9, 0xCF, 0x83, 0xCE, 0xBC, 0xCE, 0xB5} fmt.Println("合法:", utf8.Valid(bad)) // false // RuneLen 返回某个 rune 编码后占多少字节 fmt.Println("语 占字节:", utf8.RuneLen('语')) // 3 } 3.5 strings 包常用函数 package main import ( "fmt" "strings" ) func main() { s := " Hello, Go World! " // 查找 fmt.Println(strings.HasPrefix(s, " Hello")) // true fmt.Println(strings.HasSuffix(s, "! ")) // true fmt.Println(strings.Contains(s, "Go")) // true fmt.Println(strings.Index(s, "Go")) // 10(字节索引) // 裁剪 fmt.Println(strings.TrimSpace(s)) // "Hello, Go World!" fmt.Println(strings.Trim(s, " !")) // "Hello, Go World" fmt.Println(strings.TrimLeft(s, " H")) // "ello, Go World! " fmt.Println(strings.TrimSuffix("test.go", ".go")) // "test" fmt.Println(strings.TrimPrefix("cmd/test.go", "cmd/")) // "test.go" // 替换 fmt.Println(strings.Replace("aabbcc", "bb", "xx", 1)) // "aaxxcc" (只替换1次) fmt.Println(strings.ReplaceAll("aabbcc", "a", "x")) // "xxbbcc" (替换所有) fmt.Println(strings.ReplaceAll("aabbcc", "bb", "")) // "aacc" (删除子串) // 分割与合并 parts := strings.Split("a,b,c", ",") // ["a", "b", "c"] joined := strings.Join(parts, "-") // "a-b-c" fmt.Println(joined) // 大小写 fmt.Println(strings.ToUpper("hello")) // "HELLO" fmt.Println(strings.ToLower("HELLO")) // "hello" fmt.Println(strings.Title("hello go")) // "Hello Go" (1.18 后建议用 cases) // 重复与填充 fmt.Println(strings.Repeat("ab", 3)) // "ababab" fmt.Println(strings.Repeat("=", 20)) // "====================" // 字段分割(按空白字符) fields := strings.Fields(" foo bar baz ") fmt.Println(fields) // [foo bar baz] } 3.6 strconv 包:类型转换 package main import ( "fmt" "strconv" ) func main() { // ---- 整数 ---- // int -> string fmt.Println(strconv.Itoa(42)) // "42" fmt.Println(strconv.FormatInt(255, 2)) // "11111111" (二进制) // string -> int n, err := strconv.Atoi("42") if err != nil { fmt.Println("转换失败:", err) } else { fmt.Println(n) // 42 } // 更灵活的解析 n2, _ := strconv.ParseInt("FF", 16, 64) // 十六进制 -> int64 fmt.Println(n2) // 255 // ---- 布尔 ---- b, _ := strconv.ParseBool("true") fmt.Println(b) // true fmt.Println(strconv.FormatBool(true)) // "true" // ---- 浮点数 ---- f, _ := strconv.ParseFloat("3.14159", 64) fmt.Println(f) // 3.14159 // 浮点数格式化 fmt.Println(strconv.FormatFloat(3.14159, 'f', 2, 64)) // "3.14" fmt.Println(strconv.FormatFloat(0.15, 'f', 10, 64)) // "0.1500000000" fmt.Println(strconv.FormatFloat(3.14159, 'e', 4, 64)) // "3.1416e+00" } 3.7 string 与 []byte 的转换 package main import "fmt" func main() { s := "你好世界" // string -> []byte(拷贝!底层数组不共享) b := []byte(s) b[0] = 'x' // 修改不影响原字符串 fmt.Println(string(b)) // "x好世界" fmt.Println(s) // "你好世界"(原字符串不变) // string -> []rune(按字符拆分) r := []rune(s) r[0] = '我' fmt.Println(string(r)) // "我好世界" // 频繁转换的性能提示: // 如果需要反复读取 []byte,先转换一次再复用,不要反复转 } 3.8 字符串拼接:三种方式 package main import ( "fmt" "strings" ) func main() { // ---- 方式一:+ 运算符(简单场景,少量拼接) ---- hello := "Hello" + ", " + "World!" fmt.Println(hello) // 注意:每次 + 都会分配新内存并拷贝 // 循环中使用 + 拼接性能差(O(n^2)) // ---- 方式二:strings.Builder(推荐,高效) ---- var builder strings.Builder builder.Grow(100) // 预分配容量(可选但推荐) for i := 0; i < 10; i++ { builder.WriteString(fmt.Sprintf("item%d ", i)) } result := builder.String() fmt.Println(result) // "item0 item1 item2 ... item9 " // 也可以用 WriteByte, WriteRune builder.Reset() builder.WriteString("Go") builder.WriteByte('!') fmt.Println(builder.String()) // "Go!" // ---- 方式三:strings.Join(拼接已有切片) ---- items := []string{"apple", "banana", "cherry"} joined := strings.Join(items, " | ") fmt.Println(joined) // "apple | banana | cherry" } 3.9 fmt.Sprintf 格式化 package main import "fmt" func main() { // 基本格式化 fmt.Printf("整数: %d, 浮点: %f, 字符串: %s\n", 42, 3.14, "hello") // Sprintf 返回字符串而非打印 s := fmt.Sprintf("年龄: %d, 成绩: %.1f%%", 18, 95.5) fmt.Println(s) // "年龄: 18, 成绩: 95.5%" // 常用格式动词 fmt.Printf("十进制: %d\n", 255) fmt.Printf("八进制: %o\n", 255) // 377 fmt.Printf("十六进制: %x\n", 255) // ff fmt.Printf("十六进制(大写): %X\n", 255) // FF fmt.Printf("二进制: %b\n", 255) // 11111111 fmt.Printf("Unicode: %U\n", '语') // U+8BED fmt.Printf("字符: %c\n", 8364) // € (欧元符号) // 宽度与对齐 fmt.Printf("[%10s]\n", "right") // [ right] fmt.Printf("[%-10s]\n", "left") // [left ] fmt.Printf("[%05d]\n", 42) // [00042] // 结构体格式化 type Person struct { Name string Age int } p := Person{"Alice", 30} fmt.Printf("%+v\n", p) // {Name:Alice Age:30} fmt.Printf("%#v\n", p) // main.Person{Name:"Alice", Age:30} fmt.Printf("%T\n", p) // main.Person // 错误处理常用 err := fmt.Errorf("文件 %s 不存在 (行号: %d)", "config.txt", 42) fmt.Println(err) } 3.10 多行字符串 package main import "fmt" func main() { // ---- 反引号:原生字符串(推荐) ---- // 不能包含反引号本身,不处理转义符 raw := ` 第一行 第二行(有缩进) \n 这不是换行,是字面量 ` fmt.Println(raw) // ---- 双引号 + 转义 ---- escaped := "第一行\n第二行\n\t\t缩进\n路径: C:\\Users\\test" fmt.Println(escaped) // 反引号常用于:正则、SQL、模板、路径 regex := `^\d{3}-\d{4}$` fmt.Println(regex) // 不需要转义反斜杠 } 四、常见坑 坑 1:len() 返回字节数,不是字符数 s := "你好" fmt.Println(len(s)) // 6,不是 2! fmt.Println(utf8.RuneCountInString(s)) // 2(正确获取字符数) 教训:涉及中文等多字节字符时,用 utf8.RuneCountInString() 或 len([]rune(s))。 ...

2026 年 10 月 13 日

综合项目实战记录(短链接服务)

把前面四篇的东西缝合起来:一个「从需求到 Docker 部署」的完整短链接服务,走一遍 Go 工程化的真实流程 一、简述 短链接服务是一个完美的「毕业设计」:需求明确、覆盖路由/存储/并发/测试/构建全链路、不引入任何第三方 Web 框架也能跑(正适合巩固 net/http)。本文按真实开发顺序走:需求分析 → 目录设计 → 存储接口 → 短码生成 → HTTP 路由 → 计数 → 单元测试 → go mod → 构建与 Docker。 核心要点:这个项目刻意只用标准库写 HTTP(把 02-原生HTTP服务-net-http 练扎实),存储接口先给内存实现、日后可平移到 04-数据库接入-database-sql-gorm-sqlx 的 database/sql/gorm。它演示的是 Go 工程化的结构:cmd/ 装入口、internal/ 装业务、store 是抽象边界。 二、C++ 对照速查表 概念 C++ Go 关键差异 工程布局 src/ + CMake targets cmd/ + internal/ 目录即包,internal 强制隔离 抽象接口 抽象基类/接口类 interface{} 接口(store.URLStore) 鸭子类型式,一个接口多个实现 短码生成 自写 base62 自写(本项目实现) 逻辑两端一致,关注冲突处理 重定向 手写响应头 Location http.Redirect(w, r, url, 302) 标准库一行 并发安全 std::mutex / std::atomic sync.RWMutex + map 读写锁分离,并发读不死锁 单元测试 gtest/test framework testing + go test table-driven 是惯例 构建 CMake/ninja go build ./cmd/server 单二进制,静态链接 容器化 手写 Dockerfile 依赖编译环境 golang builder 镜像 + 多阶段 官方镜像成熟,CGO 关掉即纯静态 三、逐主题详解 本项目按「需求 → 结构 → 代码 → 测试 → 构建 → 部署」的顺序推进,每一小步都有可直接运行的代码片段。 ...

2026 年 10 月 13 日

STM32 GPIO 入门三连:点亮 LED、流水灯与蜂鸣器

环境配好、接线踩坑之后,接下来就是让外设动起来。这组实验全部围绕 GPIO 输出:先点亮一颗 LED,再扩展成八路流水灯,最后用同样的思路驱动蜂鸣器。三个实验的代码都在工程的 User\main.c 里(如 3-1点亮LED灯、3-2流水灯、3-3蜂鸣器)。 一、点亮第一颗 LED 之前两天都在跟接线较劲,怀疑人生;当 LED 终于亮起来的时候,成就感爆棚。核心教训:嵌入式开发,硬件接线和软件一样重要。 // PA0 配置为推挽输出 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); // 低电平点亮 LED GPIO_ResetBits(GPIOA, GPIO_Pin_0); 关键理解: GPIO 配置:推挽输出模式,速度 50MHz LED 驱动:低电平有效,GPIO_ResetBits 拉低点亮 当前状态:常亮,while(1) 为空 二、流水灯:PA0-PA7 八个 LED 在单颗 LED 的基础上,实现 PA0-PA7 八个 LED 流水灯效果。 // 配置 GPIOA 所有引脚为推挽输出 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_All; // 一次性配置所有引脚 GPIO_Init(GPIOA, &GPIO_InitStructure); // 流水灯核心:GPIO_Write 一次写入 16 位数据 while(1) { GPIO_Write(GPIOA, ~0x0001); // 0x0001取反=0xFFFE,PA0低电平点亮 Delay_ms(300); GPIO_Write(GPIOA, ~0x0002); // PA1点亮 Delay_ms(300); GPIO_Write(GPIOA, ~0x0004); // PA2点亮 // ... 依次类推到 PA7 } GPIO 操作函数对比 函数 作用 使用场景 GPIO_ResetBits 单引脚拉低 单个LED控制 GPIO_SetBits 单引脚拉高 单个LED控制 GPIO_WriteBit 按位设置 单引脚精确控制 GPIO_Write 16位整体写入 多引脚同时控制 位操作技巧 ~0x0001 = 0xFFFE → PA0 低电平(点亮),其余高电平(熄灭) ~0x0002 = 0xFFFD → PA1 低电平(点亮) 位左移:0x0001 << n 可以简化代码 优化思路 当前代码是 8 行重复,可以用循环 + 移位简化: ...

2026 年 9 月 8 日

STM32 学习 Day01:开发环境配置与面包板接线踩坑

STM32 学习第一天:配置开发环境、熟悉 ST-Link 调试器、学面包板接线。技术内容不多,但接线的两个坑值得记下来。 一、完成内容 配置 STM32 开发环境 熟悉 ST-Link 调试器使用 学习面包板接线基础 二、挫折与踩坑 挫折 1:供电原理不懂 不知道最小系统板如何为面包板供电 浪费大量时间在接线上,LED 一直不亮 教训:先搞清楚供电回路,再动手接线 挫折 2:接线思维误区 误认为面包板格子位置是唯一的对应关系 反复检查接线,始终找不到问题 顿悟:接线要看最小系统板引脚图,不是看面包板格子 这个错误卡了很长时间,一度怀疑开发板坏了 三、收获 ST-Link 调试器接线已熟练掌握 开发板接线基本熟悉 第一天的核心体会:接线问题比想象中更消耗时间——供电回路和引脚对应关系没搞清楚之前,LED 不亮根本无从判断是哪里的问题。

2026 年 9 月 7 日

C++ 服务部署实战:从源码到公网访问

在一台 2C2G 的云服务器上实测:单文件 HTTP 服务器,g++ 编译,监听 8080 端口,从编译到公网访问全流程跑通。 1. socket 七步流程 示例是一个约 60 行的单文件程序 http_server.cpp,纯 Linux socket API、零第三方依赖,放在你自己的项目目录里即可。 socket() → bind() → listen() → accept() → recv() → send() → close() 建电话 插网线 开机待命 接电话 听需求 答复 挂断 步骤 调用 说明 1 socket(AF_INET, SOCK_STREAM, 0) IPv4 + TCP,返回文件描述符(Linux 一切皆文件) 2 bind(fd, &addr, sizeof) 绑定地址;INADDR_ANY(0.0.0.0)= 监听所有网卡 3 listen(fd, 16) 开始待命;16 是 backlog 等待队列长度 4 accept(fd, ...) 阻塞等客户端,返回新 fd(每客户一个) 5 recv(fd, buf, n, 0) 读 HTTP 请求报文(原始文本) 6 send(fd, resp, len, 0) 回 HTTP 响应 7 close(fd) 挂断,回 while 等下一客 要点: ...

2026 年 8 月 25 日

C++ 后端学习路线:以云服务器为练兵场

目标:从零起步,拿一台 2C2G 的云服务器当主战场,系统学习 C++ 后端开发。 原则:基础 → 系统 → 网络 → 框架 → 工程化,渐进推进;每个阶段都在云服务器上产出可运行的作品。 总览 阶段 主题 周期 云服务器上的产出 0 Linux 基础与服务器上手 起步阶段,边用边补 workspace 工作目录、用户与权限体系 1 现代 C++ 核心 2-4 周 编译与工具链的熟练度 2 Linux 系统与网络编程 4-6 周 手写并发 HTTP 服务器 3 框架与中间件 6-8 周 Drogon + MySQL + Redis 的 REST API 4 工程化与云原生 持续 Docker 化部署 + CI/CD + 监控 时长按「每天 1-2 小时」估算,弹性理解;项目驱动,不以周数为硬指标。 阶段 0 不必一次啃完:进程管理、看日志、shell 脚本这些内容在后面每个阶段都会反复用到,随用随补即可。 阶段 0:Linux 基础与服务器上手 这一层要覆盖的内容: ...

2026 年 8 月 25 日

Linux 基础操作:目录结构、磁盘分区与权限

适用读者:有 C++/Qt 基础(熟悉 QDir / QFile),手边有一台云服务器(示例环境:Alibaba Cloud Linux,2 核 / 2G / 40G 系统盘)。 主线:每条命令都在真实服务器上跑过,边用边理解。 1. 根目录里都有什么 ls / 看到的目录(Alibaba Cloud Linux 实测): bin dev home lib64 media opt root sbin swapfile tmp var boot etc lib lost+found mnt proc run srv sys usr 这些目录不是平级无关的文件夹,而是各司其职(FHS 标准约定): 目录 放什么 类比 Windows /etc 系统与服务配置文件 C:\Windows\System32 配置 /usr 系统软件与库(最大) C:\Program Files /var 经常变化的数据:日志、缓存 C:\ProgramData 日志 /home 普通用户的家目录 C:\Users\ /root root 用户的家目录(不在 /home 里) — /tmp 临时文件,重启可能清空 %TEMP% /opt 第三方大软件 D:\Software /boot 启动引导文件 — /proc /sys /dev /run 内存虚拟文件系统,不在磁盘上 — lost+found ext4 文件系统 fsck 修复碎片(有它说明 / 是 ext4) 磁盘扫描碎片 swapfile 交换文件(小内存机器用文件代替交换分区) 虚拟内存 pagefile.sys 💡 /proc 里的数字目录就是进程 PID——内核把进程信息伪装成文件给你看,cat /proc/cpuinfo 就是读 CPU 信息。 ...

2026 年 8 月 25 日

最常用 Git 命令速查:三区模型、场景命令表与五个坑

适用人群:Git 零基础起步,以日常笔记仓库为练习场。 主线:先记住「三区模型」,命令自然就懂了。 1. 核心要点:先建立「三区」心智模型 核心要点:90% 的 Git 疑惑都源于不知道「文件现在在哪个区」。Git 有三个区域——工作区 → 暂存区 → 本地仓库,再加一个远程仓库。所有常用命令本质上就是在这些区之间搬东西。 工作区 暂存区 本地仓库 远程仓库 (你编辑的文件) (git add 后) (git commit 后) (GitHub 等) │ git add │ git commit │ git push │ ├──────────────▶├─────────────────▶ ├───────────────────▶ │ │◀──────────────┤◀───────────────── ├── ◀─────────────────┤ │ git restore │ │ git fetch / pull │ 区 是什么 对应命令 工作区 你正在编辑的目录 git status 里红色的文件 暂存区 下次 commit 要打包的内容 git add 之后变绿色的文件 本地仓库 已提交的历史(.git/ 目录) git log 能看到的 远程仓库 别人/其他机器上的副本 git push / git pull 打交道 2. 常用命令速查表(按场景) 2.1 首次配置与建仓 命令 作用 备注 git config --global user.name "xxx" 设置全局用户名 提交记录会显示它 git config --global user.email "xxx@xx.com" 设置全局邮箱 GitHub 用它关联头像/贡献图 git init 当前目录变成 Git 仓库 生成隐藏的 .git/ git clone <url> 克隆远程仓库到本地 自带远程连接 2.2 日常四件套(用得最多) 命令 作用 git status 看当前状态:哪些改了、哪些暂存了(最常打) git add <file> / git add . 把文件放入暂存区;. 表示全部 git commit -m "说明" 把暂存区内容打包成一次提交 git log --oneline 一行一条看提交历史 2.3 查看差异与历史 命令 作用 git diff 工作区 vs 暂存区的改动 git diff --staged 暂存区 vs 上次提交的改动 git show <commit> 看某次提交的具体内容 git blame <file> 看文件每行是谁、哪次提交写的 2.4 撤销与回退(救火) 命令 作用 危险度 git restore <file> 丢弃工作区改动(未 add) ⚠️ 改动不可恢复 git restore --staged <file> 取消暂存(add 反悔),保留改动 安全 git reset --soft HEAD~1 撤销上次 commit,保留改动在暂存区 安全,改说明重提最常用 git revert <commit> 生成一个「反向提交」抵消指定提交 ✅ 最安全,适合已 push 的场景 2.5 分支 命令 作用 git branch 列出本地分支,* 标记当前分支 git branch <名字> 新建分支(不切换) git switch <名字> 切换分支 git switch -c <名字> 新建并切换(= -b) git merge <分支> 把指定分支合并进当前分支 git branch -d <名字> 删除已合并的分支 2.6 远程协作 命令 作用 git remote -v 看远程仓库名和地址 git remote add origin <url> 关联一个远程仓库 git push -u origin main 首次推送并建立跟踪关系 git push 推送(之后简写即可) git fetch 只下载远程更新,不合并(先看看再说) git pull 下载并合并 = fetch + merge 3. 高频组合拳 # 场景1:日常保存进度(写笔记的标准流程) git status # 先看有什么变动 git add path/to/note.md # 只加想提交的文件 git commit -m "feat(模块): 添加 xx 笔记" git push # 场景2:commit 说明写错了,想重写 git reset --soft HEAD~1 # 退回但保留改动 git commit -m "正确的说明" # 场景3:某个文件改坏了,恢复到最后一次提交的状态 git restore 文件名 # 场景4:push 被拒(远程有新提交) git pull # 先拉下来合并 git push # 再推 # 场景5:实验性改动,开个分支玩 git switch -c try-something # ...随便折腾... git switch main # main 完全不受影响 4. 常见坑与误区 ⚠️ 坑 1:把不该提交的文件 add 进去了(如编辑器/助手的本地配置、进度笔记等本地配置层) → 现象:git status 里出现不想推的文件。 → 原因:git add . 会把所有文件一网打尽。 → 正确写法:精确指定文件名来 add;长期排除则写进 .gitignore;已被跟踪的用 git rm --cached 文件名 取消跟踪(本地文件保留)。 ...

2026 年 8 月 25 日

云服务器认识与使用:从虚拟机到公网部署

适用读者:有 C++/Qt 开发基础,用 VMware 虚拟机跑过 Linux、做过 Qt 界面开发,正在学 Go,手边有一台入门配置的云服务器(示例:2 核 / 2G / 40G)。 主线:先用起来,再理解原理。 1. 认识:你买的到底是台什么机器 核心要点:云服务器不是神秘的新东西——它就是「一台放在数据中心里、常年开机、你通过 SSH 远程使用的 Linux 电脑」,和你在 VMware 里装的那台 Ubuntu 骨子里是同一种东西。 1.1 先回忆:你在虚拟机里做过什么 用 VMware 装一个 Ubuntu 服务器版,在里面跑现成的后端程序、做 Qt 界面开发。 这台 Ubuntu 是「一台软件模拟出来的电脑」,运行在你自己的 Windows 上。 你能 ssh 连它、在里面装服务、跑程序——这些和云服务器上的操作几乎一模一样。 💡 为什么先从虚拟机讲起:你已经在「一台假的服务器」上做过大部分操作了。云服务器只是把「这台假的」换成「一台真的、别人帮你放在机房的」。 1.2 两个易混的词:「服务器」和「服务端」 词 含义 举例 服务器 (Server) 一台长期开机的电脑(硬件 + 系统) 你的云上主机、机房里的物理机 服务端 (Server-side) 一种软件的运行角色,相对客户端而言 你用 Qt 写的 TCP Server、Nginx、MySQL、Go 的 HTTP 服务 「开启服务器端」= 在某个服务器上,把一个服务程序跑起来并对外提供功能。 这个程序跑在哪台机器上都行(本机、虚拟机、云服务器),区别只在谁能访问到它——这一点正是第 2 章的核心。 1.3 一台服务器由什么组成 组成部分 作用 硬件 CPU、内存、磁盘、网卡——决定这台机器能干多重的活 操作系统 你熟悉的 Linux(Ubuntu、Alibaba Cloud Linux、CentOS 等) 服务软件 提供业务能力的程序:Nginx、MySQL、你的 Go 后端 IP / 端口 / 域名 IP 定位机器、端口定位机器上的程序、域名给人好记 2. 区别:云服务器 vs 虚拟机「开启服务器端」 核心要点:技术层面你几乎不换思路——都是「Linux + 服务程序」。真正的差别在 4 件事:谁能访问、关机了怎么办、坏了谁来修、按什么计费。 ...

2026 年 8 月 21 日