为什么学Go?其实之前就有点想学，因为听说这个编程语言，既有Python的简单和方便，又有C的运行高效。实际上第一次接触到GO,是因为向自己改一下ZeroBot-Plugin这个项目的一点点代码，也就是CPU核心读取不正常的问题，我找LLM修复了一下面向LLM编程(bushi。看了看代码，有Python的基础，挺多能看懂的。这也坚定了我想要学Golang的想法。于是，这个寒假，在整理完成歌曲库，升级完成NAS后闲得蛋疼，我决定开始学习GO。本页面会随着我的学习进度逐步更新，权当是当个笔记了。第一个Ciallo WorLd！一般编程语言学习，第一个程序都是Hello World，但是这次我向整点不一样的：package main import "fmt" func main() { fmt.Println("Ciallo World!") }另外，有一个写代码小技巧，选取多行，按Tab可以整行缩进，Shift+Tab可以反向操作。Ctrl+/可以多行进行注释，对Debug很有用。变量与数据类型局部变量和全局变量局部变量示例：package main import "fmt" func main() { var ss int = 233 fmt.Println("Ciallo World!",ss) }全局变量示例：package main import "fmt" var ss int = 233 func main() { fmt.Println("Ciallo World!",ss) }或者这样，更加简洁：package main import "fmt" var( ss = 2333 st = "sxsxsx" fl = 1234.555 fl2 = 2323E+2 ) func main() { fmt.Println("Ciallo World!",ss,st,fl.fl2) }值得注意的是，在Go中，如果变量只声明而不使用，编译就会报错。int数据类型在Go语言中，int 类型与 int8, int16, int32, 和 int64 都是用来表示整数的数据类型，但它们之间存在一些关键区别，主要体现在大小、范围和平台依赖性上。下面是这些类型的区别列表说明：类型占用空间数值范围适用场景int88位（1字节）-128 到 127需要节省内存且数值范围较小的场景int1616位（2字节）-32,768 到 32,767比int8更大的范围，但仍需控制内存使用时int3232位（4字节）-2,147,483,648 到 2,147,483,647处理更广泛的数值，例如时间戳或计算密集型应用int6464位（8字节）-9,223,372,036,854,775,808 到 9,223,372,036,854,775,807非常大的整数值或者需要精确到极大值的应用场景int取决于平台（32位系统上为4字节，在64位系统上为8字节）范围取决于系统的架构大小不考虑具体大小的通用情况，追求代码的可移植性和简洁性声明变量：直接赋值var a int8 = 10 // int8 类型变量a被赋予值10 var b int16 = -200 // int16 类型变量b被赋予值-200 var c int32 = 30000 // int32 类型变量c被赋予值30000 var d int64 = 400000 // int64 类型变量d被赋予值400000 var e int = -500 // 根据系统架构决定大小的int类型变量e被赋予值-500使用短变量声明f := int8(100) // 使用短变量声明为int8类型变量f赋值100 g := int16(-30000) // 使用短变量声明为int16类型变量g赋值-30000 h := int32(50000) // 使用短变量声明为int32类型变量h赋值50000 i := int64(600000) // 使用短变量声明为int64类型变量i赋值600000 j := int(-600) // 使用短变量声明为int类型变量j赋值-600float数据类型包括float32和float64：类型占用空间精度数值范围（大约）适用场景float3232位（4字节）小数点后约7位有效数字±1.5 × 10^-45 到 ±3.4 × 10^38对内存使用敏感且不需要高精度的场合，例如图形处理、物理仿真等需要大量浮点计算但对精度要求不是极高的应用float6464位（8字节）小数点后约15位有效数字±5.0 × 10^-324 到 ±1.7 × 10^308需要高精度的数值计算，如科学计算、金融计算等声明变量：直接赋值var k float32 = 123.456 // float32 类型变量k被赋予值123.456 var l float64 = 789.101112 // float64 类型变量l被赋予值789.101112使用短变量声明m := float32(1234.5678) // 使用短变量声明为float32类型变量m赋值1234.5678 n := float64(9876.54321) // 使用短变量声明为float64类型变量n赋值9876.54321其实赋值的时候也可以使用科学记数法：var a float = 888E+2 var b float = 888E-2上述分别的值是88800和8.88。string数据类型就是字符串嘛，常见的赋值方式有这些：直接赋值最简单直接的方式是通过字面量直接给变量赋值。s1 := "Hello, World!"使用 var 关键字声明并赋值可以使用 var 来声明一个 string 类型的变量，并且可以在声明时或之后进行赋值。var s2 string s2 = "Hello, Go!" // 或者同时声明和初始化 var s3 string = "Another Hello"多行字符串使用反引号（ ` ）可以定义多行字符串，这种方式下字符串内的换行符和其他空白字符都会被保留。s4 := `这是一个 多行 字符串`字符串拼接可以通过 + 操作符将多个字符串连接在一起。firstName := "John" lastName := "Doe" fullName := firstName + " " + lastName // 结果为 "John Doe"使用 fmt 包格式化字符串可以利用 fmt.Sprintf 函数来根据指定格式生成字符串。age := 30 description := fmt.Sprintf("My age is %d", age) // 结果为 "My age is 30"bool数据类型基本概念类型：bool取值：只有两个值，true 和 false默认值：false（如果变量声明但未赋值）声明和初始化可以使用 var 关键字来声明一个布尔变量，并且可以在声明时或之后进行赋值。var isActive bool // 声明一个布尔变量，初始值为 false isActive = true // 赋值为 true // 或者同时声明并初始化 isEnabled := false // 使用短变量声明并初始化逻辑运算符布尔值通常与逻辑运算符一起使用来进行更复杂的条件判断：逻辑与 (&&)：当两边的操作数都为 true 时，结果为 true。result := true && false // 结果为 false逻辑或 (||)：只要一边的操作数为 true，结果就为 true。result := true || false // 结果为 true逻辑非 (!)：对操作数的布尔值进行反转。result := !true // 结果为 false在控制结构中的应用布尔值最常用于控制结构如 if、for 等语句中，以决定程序的执行路径。if isActive { fmt.Println("The feature is active.") } for i := 0; i < 10 && isActive; i++ { fmt.Println(i) }比较运算符的结果比较运算符（如 ==, !=, <, >, <=, >=）的结果也是布尔值，这使得它们非常适合用于条件判断。isEqual := (5 == 5) // 结果为 true isGreater := (10 > 5) // 结果为 true基本数据类型的互相转换整型与整型之间的转换可以直接通过类型转换操作来进行不同整型之间的转换。需要注意的是，当从小范围类型向大范围类型转换时（如int8到int32）通常是安全的，反之则可能会导致数据丢失。var a int8 = 10 b := int16(a) // 将int8类型的a转换为int16类型的b整型与浮点型之间的转换可以使用类型转换将整型转换为浮点型，反之亦然。注意，从浮点型转换为整型时，小数部分会被截断。var c int = 5 d := float64(c) // 将int类型的c转换为float64类型的d var e float64 = 5.75 f := int(e) // 将float64类型的e转换为int类型的f，结果为5，小数部分被截断字符串与数字之间的转换字符串转数字：可以使用标准库中的 strconv 包提供的函数来实现。import "strconv" s := "123" i, err := strconv.Atoi(s) // 将字符串s转换为int类型的i if err != nil { // 处理错误 } f, err := strconv.ParseFloat("123.45", 64) // 将字符串"123.45"转换为float64类型的f数字转字符串：同样可以使用 strconv 包中的函数。import "strconv" num := 123 str := strconv.Itoa(num) // 将int类型的num转换为字符串 fnum := 123.45 fstr := strconv.FormatFloat(fnum, 'f', 6, 64) // 将float64类型的fnum转换为字符串，格式化参数根据需求调整指针基本概念取地址符(&)：用于获取变量的内存地址。解引用符(*)：用于访问指针所指向的变量的实际值。指针类型声明：使用*Type的形式声明一个指针类型的变量，其中Type是你希望指针指向的数据类型。代码示例下面是一个简单的例子，演示了如何声明指针、获取变量的地址以及如何通过指针访问或修改变量的值：package main import "fmt" func main() { // 声明一个整型变量a并赋值为10 a := 10 // 使用&获取变量a的地址，并将该地址赋给指针变量p var p *int = &a // 输出变量a的值、变量a的地址和指针p的内容（即a的地址） fmt.Println("Value of a:", a) fmt.Println("Address of a:", &a) fmt.Println("Value of p (address of a):", p) // 使用*来解引用指针p，以访问它指向的值 fmt.Println("Value pointed to by p:", *p) // 修改指针p指向的值（也就是修改变量a的值） *p = 20 fmt.Println("Modified value of a:", a) }在这个例子中：我们首先声明了一个整型变量a并赋予初始值10。然后，我们声明了一个指向整型的指针p，并将变量a的地址赋给了p。接下来，我们展示了如何打印出变量的值、它的地址以及指针变量的内容。最后，通过解引用指针p，我们直接修改了a的值，证明了通过指针可以直接访问和修改其指向的变量的值。变量是否允许跨包使用在变量命名中，如果是小写字母开头的变量，则只允许在当前的包中使用，类似于私有变量。但是，如果是大写字母开头的变量，则可以在别的地方通过导入包进行使用。例如：var miku int = 39 var Rin int = 12miku变量只能在当前包中的代码读取和使用，而Rin变量别的地方导入了这个包的话就可以使用。设置自己的包创建一个新的目录来存放你的包文件。例如，你可以创建一个名为mypackage的目录。将plugin.go移动到这个新目录中。在plugin.go中声明包名。如果你希望包名与目录名一致，则可以这样写：package mypackage在test.go中导入你刚刚创建的包，并使用其中的变量。5.初始化Go模块下面是一个具体的例子：项目结构如下：myproject/ ├── test.go └── mypackage/ └── plugin.go首先，在plugin.go中定义一个公共变量（注意：Go中的变量如果要导出，首字母必须大写）：package mypackage var MyVariable = "Hello from plugin.go"然后，在test.go中导入mypackage并使用MyVariable:package main import ( "fmt" "myproject/mypackage" ) func main() { fmt.Println(mypackage.MyVariable) } 然后进入到该目录，初始化Go模块：go mod init myproject运算符常见运算符算术运算符：package main import "fmt" func main() { a := 10 b := 5 fmt.Println("a + b =", a + b) // 加法：15 fmt.Println("a - b =", a - b) // 减法：5 fmt.Println("a * b =", a * b) // 乘法：50 fmt.Println("a / b =", a / b) // 除法：2 fmt.Println("a % b =", a % b) // 取模：0 }关系运算符：package main import "fmt" func main() { a := 10 b := 5 fmt.Println("a == b is", a == b) // false fmt.Println("a != b is", a != b) // true fmt.Println("a < b is", a < b) // false fmt.Println("a > b is", a > b) // true fmt.Println("a <= b is", a <= b) // false fmt.Println("a >= b is", a >= b) // true }逻辑运算符：package main import "fmt" func main() { a := true b := false fmt.Println("a && b is", a && b) // false fmt.Println("a || b is", a || b) // true fmt.Println("!a is", !a) // false }位运算符：package main import "fmt" func main() { a := 60 // 二进制: 0011 1100 b := 13 // 二进制: 0000 1101 fmt.Printf("a & b = %d\n", a & b) // 按位与: 12 (0000 1100) fmt.Printf("a | b = %d\n", a | b) // 按位或: 61 (0011 1101) fmt.Printf("a ^ b = %d\n", a ^ b) // 按位异或: 49 (0011 0001) fmt.Printf("a << 2 = %d\n", a << 2) // 左移两位: 240 (1111 0000) fmt.Printf("a >> 2 = %d\n", a >> 2) // 右移两位: 15 (0000 1111) }赋值运算符：package main import "fmt" func main() { var a int = 5 a += 3 // 相当于 a = a + 3 fmt.Println("a += 3 =", a) // 8 a -= 2 // 相当于 a = a - 2 fmt.Println("a -= 2 =", a) // 6 a *= 2 // 相当于 a = a * 2 fmt.Println("a *= 2 =", a) // 12 a /= 3 // 相当于 a = a / 3 fmt.Println("a /= 3 =", a) // 4 a %= 3 // 相当于 a = a % 3 fmt.Println("a %= 3 =", a) // 1 }获取用户输入1. fmt.Scanffmt.Scanf允许你按照指定的格式字符串来解析输入。它的工作方式类似于C语言中的scanf函数。package main import ( "fmt" ) func main() { var name string var age int fmt.Print("请输入你的名字和年龄: ") n, err := fmt.Scanf("%s %d", &name, &age) if err != nil { fmt.Println("读取失败:", err) } else { fmt.Printf("成功读取了 %d 个值 - 名字: %s, 年龄: %d\n", n, name, age) } }注意：%s只读取一个单词，即遇到空格就会停止。2. fmt.Scanlnfmt.Scanln与fmt.Scan类似，但它会在遇到换行符时停止扫描，并丢弃输入中的任何未读取的部分。这使得Scanln非常适合于读取整行输入，其中输入项由空格分隔。package main import ( "fmt" ) func main() { var name string var age int fmt.Print("请输入你的名字和年龄: ") n, err := fmt.Scanln(&name, &age) if err != nil { fmt.Println("读取失败:", err) } else { fmt.Printf("成功读取了 %d 个值 - 名字: %s, 年龄: %d\n", n, name, age) } }与Scanf不同的是，Scanln不需要格式化字符串来指示如何解析输入，它自动根据变量类型解析输入。区别fmt.Scanf：需要提供格式化字符串来指定输入应该如何被解析。适用于你需要精确控制如何解释输入的情况。fmt.Scanln：不需要格式化字符串，它会自动根据参数类型解析输入，直到遇到换行符为止。更适用于简单的输入场景。fmt.Scan：与Scanln相似，但不会跳过末尾的换行符，并且可以在一行内读取多个值，不等待换行。流程控制if 流程控制1. 单分支 if单分支的if语句仅包含一个条件判断和相应的执行代码块。如果条件为真，则执行该代码块；否则跳过。package main import "fmt" func main() { age := 20 if age >= 18 { fmt.Println("成年人") } }2. 双分支 if-else双分支的if-else语句除了有一个条件判断和相应的执行代码块外，还提供了一个else部分，当条件为假时执行。package main import "fmt" func main() { age := 16 if age >= 18 { fmt.Println("成年人") } else { fmt.Println("未成年人") } }3. 多分支 if-else if-else多分支的if-else if-else语句允许你检查多个条件，并对每个条件执行不同的代码块。一旦某个条件为真，相应的代码块被执行，其余的条件将不会被检查。package main import "fmt" func main() { score := 85 if score >= 90 { fmt.Println("优秀") } else if score >= 75 { fmt.Println("良好") } else if score >= 60 { fmt.Println("及格") } else { fmt.Println("不及格") } }switch 流程控制基本用法package main import ( "fmt" ) func main() { i := 2 switch i { case 1: fmt.Println("数字是1") case 2: fmt.Println("数字是2") case 3: fmt.Println("数字是3") default: fmt.Println("不是1, 2, 或3") } }使用表达式作为switch的条件switch不仅可以基于整数或字符串等简单类型的比较，还可以用于更复杂的表达式。package main import ( "fmt" ) func main() { a, b := 12, 6 switch { case a+b == 18: fmt.Println("a + b 等于18") case a-b == 6: fmt.Println("a - b 等于6") default: fmt.Println("没有匹配的情况") } }fallthrough 关键字如果你想在匹配的case后继续执行下一个case的代码，可以使用fallthrough。package main import ( "fmt" ) func main() { i := 1 switch i { case 0: fmt.Println("i 是0") fallthrough // 即使i不是0，也会尝试执行下一个case case 1: fmt.Println("i 是1或者从上一个case fallthrough过来") default: fmt.Println("i 不是0也不是1") } }switch 表达式类型为字符串switch语句也可以直接用于字符串比较。package main import ( "fmt" ) func main() { str := "hello" switch str { case "hello": fmt.Println("你好") case "world": fmt.Println("世界") default: fmt.Println("未知字符串") } }循环基本 for 循环最基本的for循环形式包括初始化语句、条件表达式以及后续操作（通常用于递增或递减计数器）。这种形式与C或Java中的for循环相似。package main import "fmt" func main() { sum := 0 for i := 0; i < 10; i++ { sum += i } fmt.Println("Sum:", sum) }在这个例子中，循环从i=0开始，每次循环i增加1，直到i不小于10为止。省略部分的 for 循环仅条件表达式：如果省略了初始化和后续操作，for循环就类似于其他语言中的while循环。package main import "fmt" func main() { i := 0 for i < 3 { fmt.Println(i) i++ } }无限循环：如果省略所有部分，就是一个无限循环。可以通过break语句来退出循环。package main import "fmt" func main() { for { fmt.Println("This will loop forever") break // 使用break退出循环 } }for range 循环for range提供了一种简洁的方式来遍历数组、切片、字符串、映射和通道。对于每种类型，range返回的值有所不同。遍历数组或切片package main import "fmt" func main() { nums := []int{2, 3, 4} for index, value := range nums { fmt.Printf("Index: %d, Value: %d\n", index, value) } }遍历字符串package main import "fmt" func main() { str := "hello" for i, c := range str { fmt.Printf("Character at position %d is %c\n", i, c) } }遍历映射package main import "fmt" func main() { m := map[string]int{"apple": 2, "banana": 3} for key, value := range m { fmt.Printf("%s -> %d\n", key, value) } }注意：遍历映射时，键值对的顺序不是固定的。控制流程关键字1. breakbreak 语句用来终止最内层的 for、switch 或 select 语句，并跳出该结构。它常用于提前退出循环或选择结构。package main import "fmt" func main() { for i := 0; i < 10; i++ { if i == 3 { break // 当i等于3时，跳出循环 } fmt.Println("Number:", i) } fmt.Println("Loop finished") }注意事项: break 只能用于终止最近的循环或选择结构。如果需要从嵌套多层的循环中跳出，可以给循环命名并用标签指定要跳出的循环。2. continuecontinue 语句用于跳过当前循环体中剩余的语句，并继续执行下一次循环（如果有的话）。它通常用于忽略某些特定情况下的后续逻辑。package main import "fmt" func main() { for i := 0; i < 5; i++ { if i == 2 { continue // 当i等于2时，跳过本次循环的剩余部分 } fmt.Println("Number:", i) } }注意事项: continue 同样只能影响最近的循环。对于复杂的嵌套结构，可能需要更清晰的逻辑设计来避免混淆。3. gotogoto 语句允许程序直接跳转到由标签标记的地方。虽然它提供了灵活的控制流，但过度使用会导致代码难以理解和维护。package main import "fmt" func main() { i := 0 start: if i < 5 { fmt.Println("Number:", i) i++ goto start // 跳转到名为start的标签处 } }注意事项: Go语言鼓励限制使用goto，因为它可能导致“意大利面条式”代码（即代码逻辑混乱，难以跟踪）。一般情况下，应尽量使用结构化的控制流工具如for、if等代替。4. returnreturn 语句用于从函数中返回，可选地带有一个返回值（对于有返回值的函数）。它是结束函数执行的标准方式。package main import "fmt" func add(a, b int) int { return a + b // 返回a+b的结果 } func main() { result := add(3, 4) fmt.Println("Result:", result) // 使用return提前结束main函数 if result > 5 { fmt.Println("结果大于5") return } }注意事项: 在定义了返回类型的函数中，必须确保每次执行路径都有一个return语句，否则会导致编译错误。同时，return语句可用于在满足特定条件时提前终止函数执行。函数与包函数基本函数定义与调用首先，我们来看一个简单的函数定义和调用的例子：package main import "fmt" // 定义一个名为add的函数，接受两个int类型的参数，返回它们的和 func add(a int, b int) int { return a + b } func main() { // 调用add函数并打印结果 result := add(5, 3) fmt.Println("5 + 3 =", result) }函数的重要特性多重返回值：Go语言允许函数返回多个值。这在错误处理等方面非常有用。func divide(a, b int) (int, error) { if b == 0 { return 0, fmt.Errorf("cannot divide by zero") } return a / b, nil }命名返回值：可以为返回值指定名称，并且可以直接从函数体内部返回这些值。func profile(name string) (fullname string) { fullname = name + " Doe" return // 省略了返回值，但会返回fullname变量的当前值 }匿名函数与闭包：Go支持匿名函数，可以作为参数传递或赋值给变量。func main() { // 定义一个匿名函数并立即执行 func(msg string) { fmt.Println(msg) }("Hello, World!") // 或者将其赋值给变量后调用 sayHello := func(msg string) { fmt.Println(msg) } sayHello("Hello again!") }可变参数函数：类似于其他语言中的varargs，Go语言允许函数接受不定数量的参数。func sum(nums ...int) int { total := 0 for _, num := range nums { total += num } return total } func main() { fmt.Println(sum(1, 2, 3)) // 输出6 }defer语句：用于安排一个函数调用在包含它的函数返回之后执行，常用于资源清理工作。func main() { defer fmt.Println("World") fmt.Println("Hello") } // 输出顺序是先"Hello"后"World"包包的定义首先，每个.go文件必须以package声明开头，这表示该文件属于哪个包。例如，创建一个名为mathutil的包：// 文件名: mathutil.go package mathutil import "fmt" // 定义一个公共函数（注意首字母大写） func Add(a, b int) int { return a + b } // 私有函数（首字母小写），仅在包内可见 func privateAdd(a, b int) int { fmt.Println("This is a private function.") return a + b }在这个例子中，Add函数因为其名字以大写字母开头，所以它是公共的，可以从其他包中访问。相反，privateAdd函数的名字以小写字母开头，因此它是私有的，只能在mathutil包内部使用。包的导入和使用要使用其他包中的功能，需要使用import关键字导入相应的包。继续上面的例子，我们可以在另一个文件中导入并使用mathutil包：// 文件名: main.go package main import ( "fmt" "path/to/mathutil" // 假设mathutil位于/path/to/目录下 ) func main() { result := mathutil.Add(10, 20) fmt.Printf("The result of adding is %d\n", result) }请注意，在导入自定义包时，你需要提供相对于$GOPATH/src或Go modules根目录的路径。包的别名当你想为导入的包指定一个简短或不同的名称时，可以使用别名。示例：包别名// 文件: main.go package main import ( m "path/to/mypackage" // 使用m作为mypackage的别名 ) func main() { result := m.Add(5, 3) println(result) }包的目录结构安排通常，每个独立的功能模块都应该有自己的目录，并且在这个目录下包含一个或多个.go文件，它们都声明相同的包名。也就是一个文件夹下的.go文件归为一个包。推荐的项目结构：myproject/ ├── cmd/ │ └── myapp/ │ └── main.go ├── pkg/ │ └── mypackage/ │ ├── add.go │ └── subtract.go └── go.modcmd/：存放应用的入口点。pkg/：存放可重用的包。mypackage为包名，导入的时候采用这个名字，但是实际调用函数的时候，以包中定义的package xxx为准。例如：ccc = xxx.Add(1,2)init 函数在Go语言中，init函数是一个特殊的函数，它不需要调用即可自动执行。每个.go文件可以包含一个或多个init函数，这些函数主要用于初始化操作，如设置变量的初始值、验证配置等。下面将详细介绍init函数的工作机制、使用场景以及一些需要注意的细节。init函数的基本特性自动执行：init函数无需显式调用，编译器会确保在程序启动时自动执行。无参数和返回值：init函数不能接受任何参数，也不能有返回值。包级别初始化：init函数用于包级别的初始化，这意味着它们会在导入包时被调用，但仅限于该包内部。执行顺序：在同一个文件内，init函数按照它们声明的顺序执行。如果一个包中有多个文件，init函数按文件名的字典顺序执行。包的所有init函数执行完毕后，才会执行导入它的包中的init函数。使用场景初始化不可变变量：对于需要复杂计算才能确定其值的常量，可以在init函数中进行计算并赋值。验证配置：可以在程序运行之前检查配置的有效性。资源加载：如数据库连接、配置文件读取等。示例代码以下是一个简单的示例，展示了如何使用init函数：package main import "fmt" var globalVar string func init() { fmt.Println("First init function called.") globalVar = "Initialized" } func init() { fmt.Println("Second init function called.") if globalVar != "Initialized" { fmt.Println("globalVar was not properly initialized!") } } func main() { fmt.Println("Main function called.") fmt.Printf("Global var: %s\n", globalVar) }输出结果为：First init function called. Second init function called. Main function called. Global var: Initialized注意事项避免过度使用：虽然init函数很方便，但过度依赖它可能会导致代码难以理解和维护。尽量让初始化逻辑清晰明确，特别是在大型项目中。依赖管理：由于init函数会自动执行，如果存在依赖关系（例如一个包的初始化依赖另一个包的初始化），需要小心处理初始化顺序，以避免潜在的问题。测试困难：由于init函数是隐式执行的，这可能使得某些类型的单元测试变得复杂。考虑将初始化逻辑移到显式的函数中，以便更容易地进行测试。匿名函数在Go语言中，匿名函数（Anonymous Function）是指那些未命名的函数，它们可以直接赋值给变量、作为参数传递给其他函数或在需要的地方直接定义和使用。匿名函数提供了极大的灵活性，特别是在需要短生命周期的函数或者当函数逻辑较为简单且仅需使用一次时。匿名函数的基本形式匿名函数的语法与普通函数相似，但没有函数名。以下是匿名函数的基本结构：func(参数列表) 返回值类型 { 函数体 }例如，一个接受两个整数并返回它们之和的匿名函数可以这样定义：sum := func(a, b int) int { return a + b }在这个例子中，sum变量现在指向这个匿名函数，可以通过sum(3, 4)来调用它。立即执行的匿名函数在Go中，还可以定义后立即执行匿名函数。这种模式通常用于避免变量泄漏到外部作用域，或者当你只需要一次性使用的函数时非常有用。要立即执行匿名函数，只需在定义后加上圆括号并传入参数（如果有的话）：result := func(a, b int) int { return a + b }(5, 3) fmt.Println("Result:", result) // 输出: Result: 8闭包闭包（Closure）是编程中的一个重要概念，尤其是在支持匿名函数的语言中，如Go语言。理解闭包可以帮助你编写更加灵活和模块化的代码。下面我将尽量简单详细地解释什么是闭包，以及如何在Go语言中使用它们。什么是闭包？简而言之，闭包是一个函数加上该函数创建时的环境。这里的“环境”指的是函数定义时可以访问到的所有变量。换句话说，即使函数在其原始作用域之外被调用，它仍然能够记住并访问其创建时的作用域中的变量。为什么需要闭包？闭包允许我们封装值，并在不同的上下文中使用这些封装的值。这在处理回调函数、延迟计算或封装私有数据时特别有用。Go语言中的闭包示例考虑一个简单的例子，展示如何在Go中创建和使用闭包：package main import "fmt" // makeAdder 返回一个匿名函数，这个匿名函数形成一个闭包 func makeAdder() func(int) int { sum := 0 // 这个变量是makeAdder内部的局部变量 return func(x int) int { sum += x // 匿名函数可以访问并修改sum return sum } } func main() { adder := makeAdder() // adder现在指向一个闭包 fmt.Println(adder(1)) // 输出: 1 fmt.Println(adder(2)) // 输出: 3 fmt.Println(adder(3)) // 输出: 6 }在这个例子中，makeAdder函数返回了一个匿名函数，这个匿名函数形成了一个闭包。闭包捕获了makeAdder函数内部的sum变量。因此，每次调用adder函数时，它都能记住之前的sum值，并在此基础上进行累加。闭包的工作原理外部函数执行完毕后，通常情况下其局部变量会被销毁。但是，如果有一个闭包引用了这些局部变量，那么这些变量将不会被销毁，而是继续存在，直到不再有任何引用为止。在上面的例子中，尽管makeAdder函数已经执行完毕，但由于返回的匿名函数（闭包）仍然持有对sum变量的引用，所以sum变量会持续存在于内存中，供闭包使用。defer 关键字在Go语言中，defer关键字用于延迟函数或方法的执行直到其所在的goroutine即将结束。通常，defer用于简化函数调用后的清理代码，比如关闭文件、解锁互斥锁等。当有多个defer语句时，它们会按照后进先出（LIFO）的顺序执行。基本用法考虑一个简单的例子，其中我们想要确保在函数结束前打印一条消息。package main import "fmt" func main() { defer fmt.Println("世界") fmt.Println("你好") }输出将是：你好 世界这里，尽管defer fmt.Println("世界")写在了前面，但由于defer的作用，它会在包含它的函数main结束之前才执行。多个defer语句当你在一个函数中有多个defer语句时，它们会以LIFO顺序执行。package main import "fmt" func main() { for i := 0; i < 5; i++ { defer fmt.Print(i) } }这段代码将输出：43210可以看到，数字是按相反顺序打印出来的，这是因为每个defer语句都是在循环迭代结束时被安排执行，并且按照后进先出的顺序执行。使用defer进行资源管理defer最常用的场景之一是在打开资源（如文件、数据库连接等）之后立即计划如何释放这些资源，即使在处理过程中出现错误也能保证资源得到正确释放。package main import ( "fmt" "os" ) func main() { file, err := os.Open("test.txt") if err != nil { fmt.Println("Error:", err) return } // 确保在函数退出时关闭文件 defer file.Close() // 执行文件操作... }在这个例子中，无论文件操作是否成功，file.Close()都会被执行，从而避免了资源泄露。错误处理利用defer和recover来处理错误package main import ( "fmt" "log" ) // 定义一个函数用于执行可能抛出panic的操作 func divide(a, b int) { // 使用defer与recover来捕获panic defer func() { if r := recover(); r != nil { // 记录错误信息 log.Printf("Recovered in divide: %v", r) } }() // 执行除法操作 result := a / b fmt.Printf("%d / %d = %d\n", a, b, result) } func main() { // 正常情况 divide(10, 2) // 可能造成除以0的情况 divide(10, 0) // 确认程序继续执行 fmt.Println("Program continues after potential panic.") }代码解释：recover()：在Go中，当发生panic时，可以使用recover来捕获并恢复正常执行流程。它必须在一个延迟调用的函数内部调用。defer：defer语句会将函数推迟到外层函数返回之后执行。在这个例子中，我们将包含recover的匿名函数推迟执行，以便在发生panic时能够恢复控制。在divide函数中，如果第二个参数b为0，则会发生除以零的运行时错误，这会导致一个panic。但是，因为我们使用了defer和recover，这个panic会被捕获，相应的错误消息也会被记录下来，而不会导致整个程序停止执行。在main函数中，我们首先正常地调用了divide函数，然后尝试用0作为除数再次调用它，以此展示即使遇到可能导致程序崩溃的错误，我们的程序仍能继续执行。自定义错误自定义错误但程序正常运行package main import ( "errors" "fmt" ) // 自定义一个函数，它可能会返回一个错误 func divide(a, b int) (int, error) { if b == 0 { return 0, errors.New("cannot divide by zero") } return a / b, nil } func main() { // 正常情况 result, err := divide(10, 2) if err != nil { fmt.Println("Error:", err) } else { fmt.Printf("Result: %d\n", result) } // 可能导致错误的情况 result, err = divide(10, 0) if err != nil { fmt.Println("Error:", err) } else { fmt.Printf("Result: %d\n", result) } // 确认程序继续执行 fmt.Println("Program continues after handling the error.") }注释：在divide函数中，我们检查了除数是否为0。如果是，则返回一个自定义的错误信息。在main函数中，我们通过检查返回的错误值来决定下一步操作。即使发生了错误（如除以零），程序仍然能够继续执行。直接抛出错误终止程序package main import ( "fmt" ) // 自定义一个函数，当遇到错误时直接抛出异常终止程序 func divide(a, b int) { if b == 0 { panic("division by zero") // 直接抛出错误并终止程序 } fmt.Printf("%d / %d = %d\n", a, b, a/b) } func main() { // 正常情况 divide(10, 2) // 这将导致程序崩溃 divide(10, 0) // 下面这条消息不会被打印，因为程序已经由于panic而终止 fmt.Println("This message will not be printed.") }注释：在这个例子中，当尝试除以零时，divide函数使用panic来抛出一个运行时错误，并立即终止程序的执行。panic是一个内置函数，用于停止常规的控制流。一旦panic被调用，程序会停止当前函数的执行，并开始回溯goroutine的调用栈，直到找到一个recover（如果没有recover，则程序终止）。因此，在main函数中，当divide(10, 0)被调用时，程序会立即终止，不会执行后续的任何代码（例如最后的fmt.Println语句）。
主板是弯的？！偶然一次机会，我拆开了我的NAS。然后发现，因为机箱太过劣质，主板安装的位置有一点变形，主板已经被槁弯了。大无语。于是我就想换一个机箱。正好放假了，我就和我朋友去大战电脑城，20的价格搞下了联想的启天M5900机箱。后面去淘宝查了一下，这个机箱基本都要50，还不包邮，运费都要35。这波大战电脑城血赚！众所周知，品牌机的机箱一般做工都会比较扎实，并且有不错的风道，这就是我选择捡垃圾品牌机机箱的原因。不得不说，这个机箱确实做工扎实，外观沉稳大气。挺不错的。前面板通透的散热孔，这也是为什么我选择这款机箱的原因。2017年12月出厂，不算特别老古董。但是不知道为什么我收回了外观成色这么差，内部倒还挺干净。确定升级计划机箱已经搞到手了，接下来就是硬盘了。原来的数据盘是两块3TB的硬盘组的RAID1。而我后面在有了NAS后，中奖获得了一块4TB的机械硬盘，再加上之前有一个UP送的4TB硬盘，这下不就可以组RAID1了吗？虽然但是，这两块硬盘，一块是日立，128MB缓存；另一个是东芝，64MB缓存。但容量是一样的，所以理论上应该是没问题的。至于系统盘，我打算抽掉我机架服务器的1TB希捷酷鱼。其实前面说的两块4TB的硬盘也是从机架服务器抽的，因为用不到，装上去一直闲置，有一个6TB的SAS盘还没用到一半。。。至于换下来的3TB硬盘？我的想法是丢到我另一台电脑上面去作备份盘，还有一块就插到我的机架服务器里面。唯一不爽的是，我家里云128天的Uptime要被打破了。进行升级确定了计划，那我们就开干吧！清理干净捡来的机箱，正是开始工作！改跳线联想的机箱有一个很阴间的点，那就是跳线不是标准的跳线规格。由于我不需要前置的USB和音频，这两个不接就不接吧，没什么问题。但是开关，指示灯总得接上吧？其实不接也行，设置来电自启就是了（X）反正就三组线：开关 电源灯 硬盘灯 去网上查一下联想机箱或者联想主板跳线的定义，直接自己魔改就行。因为原来的跳线是杜邦线插在了定制的插头上，用细小的螺丝刀挑出来，然后也把旧机箱的插头挑出来，再根据定义，把线插到普通主板跳线接头对应的位置，就行了。当然，你也可以去淘宝几块钱买一套转接线。拆旧的NAS一打开，灰尘，灰尘，还是他妈的灰尘！！！太离谱了，开机一年的NAS，内部简直是出土文物！！！出土的风扇：出土的数据盘：这就是直接用风扇对着硬盘吹的代价，温度很好看，即是夏天，，但硬盘恐怕不好看。装机测试过程我就省略了，装机这么简单的事情，谁不会啊？看看效果吧：为了保护数据盘，我的两个数据盘安装在原生的3.5寸硬盘位上面。系统盘通过支架装在了光驱位，固体硬盘就直接吊着了，反正固态硬盘无所谓。不多不少，刚刚好。后面测试了一下，待机功耗变低了一些，应该是硬盘的功劳。当然，我去掉了一个风扇，应该也有影响。从40W变成37.5w了：迁移数据我用的CloneZilla进行迁移，选择device to device模式。过程很简单，跟着提示走就行了。后面启动系统后在cockpit控制台把/home的分区拉大就行了。我只是迁移系统盘，数据盘需要重新组阵列然后再用rsync迁移数据。把之前的两个数据盘插到另一台电脑，然后直接：rsync -aAXvz -e ssh --info=progress2 /mnt/data root@192.168.1.233:/mnt/4T-RAID1/这里我是按照权限的设置，SELinux的标签，全部都同步过去了，避免兼容性问题。最后，各项服务成功启动，迁移完成！投入使用的效果拆开侧板和前面板：后续：加了个风扇虽然说品牌机机箱风道很不错，但是没有风扇光有风道也没用。只靠电源的那个小风扇没办法降温，于是我在某宝花了7块钱搞了个磁悬浮风扇，外加5块钱搞了个PWM调速器。再找个挡板，魔改一下，完美。调速旋钮伸出机箱，这样调节的时候就不需要拆开了。
自建Jellyfin私人媒体服务器，告别屌毛网易云！
Jellyfin私人媒体服务器搭建指南成功搭建了jellyfin服务器，拥有了自己的私人媒体库，记录一下折腾过程。起因∶网易云无故被封促进我自建媒体库的原因，是因为网易云乱封号，两次把我的号给封了。理由居然是——播放数据异常真就是，想封你，随便编个理由，直接就封。纯脑残！其实之前我就有点想搞媒体服务器了，但是由于我看番都是用种子下载看番，而且番剧看一次也就过去了，我觉得也没用留着的必要。毕竟番剧的一话，1080P 60帧，都要差不多1GB了，看番多的话，文件破TB级那是轻轻松松。这次封号，算是坚定了我的想法。我觉得国内互联网公司绝大部分都是狗屎，让人无语。其实是2024年10月封的号，但是我2025年1月才开始折腾媒体服务器。因为之前都在学校，课业繁忙，乐的要死，因为整理音乐库费时费力，所以我打算放假再搞。事实证明我的想法是对的，我前前后后整理了快一个星期，整理了绝大部分，但是还有相当一部分歌曲还没整理。太累了，让我歇歇。目前已经整理了1300+首歌，文件夹已经90G+了。。。为什么选择了Jellyfin？免费，开源。开源，开源，开源，还是他妈的开源！我是支持GNU自由软件运动的人，自然更倾向于使用开源的解决方案，尤其是使用GPL协议的开源软件。顺带一提，我主力操作系统早就已经换成GNU/Linux了。部署Jellyfin下载Jellyfin直接前往官方网页下载即可，我用的最新版，目前是10.10.3。下载链接：repo.jellyfin.org/?path=/server/linux/latest-stable下载FFmpeg由于我服务器使用的是AlmaLinux8.10，是服务器发行版，其自带的FFmpeg比较老，新版的Jellyfin并不兼容。虽然可以通过yum包管理器直接安装对应版本的Jellyfin，并且不会出现兼容问题。但是旧版的Jellyfin并没有歌词支持，对我来说比较蛋疼，因为我主要想做的就是歌曲的媒体服务器。我下载的是7.0.2版本的，这种的话，我更倾向于使用最新版的前一个稳定版。下载地址：ffmpeg.org/releases/ffmpeg-7.0.2.tar.xz自定义环境变量，以使用自定义的ffmpeg我的做法是，把FFmpeg和Jellyfin解压后，分别放到了不同的文件夹，然后在根目录文件夹放了一个启动脚本。以下是启动脚本的内容：#!/bin/bash # 定义FFmpeg二进制文件所在的路径 FFMPEG_PATH="./ffmpeg/bin" # 将FFmpeg路径添加到PATH环境变量的最前面 export PATH="$FFMPEG_PATH:$PATH" # 启动Jellyfin ./jellyfin/jellyfin脚本保存为run.sh，然后再：chmod +x ./run.sh试一下执行这个脚本，看看Jellyfin有没有启动成功，如果启动成功后就完事了。配置Jellyfin初始化配置打开浏览器，输入服务器对应的ip+8096端口，例如：127.0.0.1:8096按照指引，设置用户名和密码。这部过于简单，有手就行，不多赘述。配置HTTPS访问这个Jellyfin比较特别，需要使用pfx证书，而不是常见的pem+key的证书组合。我们需要把pem和key证书转成pfx格式。下面是一个shell脚本，可以便利当前目录的pem+key证书，并且转换为pfx证书放到当前目录的pfx_cert/文件夹内：#!/bin/bash # 定义输出目录 OUTPUT_DIR="pfx_cert" # 创建输出目录（如果它不存在） mkdir -p "$OUTPUT_DIR" # 遍历所有的 .key 文件 for key in *.key; do # 获取不带扩展名的文件名 base_name="${key%.key}" # 检查对应的 .pem 文件是否存在 if [[ -f "${base_name}.pem" ]]; then echo "正在处理 $base_name..." # 转换为 PFX 格式 openssl pkcs12 -export \ -out "${OUTPUT_DIR}/${base_name}.pfx" \ -inkey "$key" \ -in "${base_name}.pem" \ -certfile le-chain.pem echo "已生成 ${OUTPUT_DIR}/${base_name}.pfx" else echo "警告：找不到与 $key 对应的 .pem 文件" fi done把以上内容保存到to_pfx.sh，然后：chmod +x ./to_pfx.sh ./to_pfx.sh当要求输入密码的时候，直接回车，不设置密码。在Jellyfin设置证书，选择得到的pfx证书文件。记得勾上强制HTTPS和允许远程连接保存后，重启Jellyfin，应该就可以通过HTTPS访问了。别忘了添加域名解析记录。至此，基本上配置完成。添加媒体注意事项如果是音乐的话，最好的目录分类方式应该是：歌手 > 专辑 > 音乐文件/歌词文件例如，如果是Taylor Swift的《Red》专辑，目录结构就长这样：/path/to/music_library/ │ ├── Taylor Swift │ └── Red (2012) │ ├── 01. State of Grace.mp3 │ ├── 01. State of Grace.lrc │ ├── 02. Red.mp3 │ ├── 02. Red.lrc │ ├── 03. Treacherous.mp3 │ ├── 03. Treacherous.lrc │ ├── 04. I Knew You Were Trouble.mp3 │ ├── 04. I Knew You Were Trouble.lrc │ ├── 05. All Too Well.mp3 │ ├── 05. All Too Well.lrc │ ├── 06. 22.mp3 │ ├── 06. 22.lrc │ ├── 07. I Almost Do.mp3 │ ├── 07. I Almost Do.lrc │ ├── 08. We Are Never Ever Getting Back Together.mp3 │ ├── 08. We Are Never Ever Getting Back Together.lrc │ ├── 09. Stay Stay Stay.mp3 │ ├── 09. Stay Stay Stay.lrc │ ├── 10. The Last Time.mp3 │ ├── 10. The Last Time.lrc │ ├── 11. Holy Ground.mp3 │ ├── 11. Holy Ground.lrc │ ├── 12. Sad Beautiful Tragic.mp3 │ ├── 12. Sad Beautiful Tragic.lrc │ ├── 13. Begin Again.mp3 │ ├── 13. Begin Again.lrc │ ├── 14. Girl At Home.mp3 │ ├── 14. Girl At Home.lrc │ └── cover.jpg # 封面图片整理好后，在Jellyfin添加媒体库，选择音乐，在添加上对应的文件夹路径，就可以了。如果是按照这个方法添加的话，Jellyfin是可以自动检索元数据并且添加的，可以改善音乐库的观感：歌词的效果：效果还是可以的！音乐库去哪找？我是用了网易云无损解析这类工具，把歌曲一首一首下回来，自己整理，工作量巨大！好处是，有一些工具可以一并把歌词下来，比较舒服。这种工具估计网上一搜，都一大把，很多都是网站在线解析的，并不需要下载软件。什么？你不会找？找不到？是不是搜索引擎是垃圾百度啊？还是360搜索？如果还在用国内这种垃圾搜索引擎，还不会换，那我只能说你是赛博文盲！当然，你也可以去某宝，花几块钱去购买并下载资源，但是大概率给你个百度网盘链接（改名百度软盘得了），然后下载龟速。2333我的收集方法我是用了这个:api.toubiec.cn/wyapi.html 我是一次下一个专辑，由于下载的每首歌都是一个zip压缩包，本来下载就费时费力，整理更加费时费力。于是，我就整了一点小工具做一下辅助：open_zip.sh放在下载目录，负责遍历解压压缩包，文件内容如下：#!/bin/bash # 检查是否有zip文件存在 if [ "$(ls -A *.zip 2> /dev/null)" ]; then # 遍历所有.zip文件 for zip_file in *.zip; do # 获取不带扩展名的文件名 base_name="${zip_file%.zip}" # 创建以文件名为名的新文件夹 mkdir -p "$base_name" # 解压到新建的文件夹内 unzip -q "$zip_file" -d "$base_name" # 检查解压是否成功 if [ $? -eq 0 ]; then echo "Extracted $zip_file into folder $base_name" # 如果解压成功，删除zip文件 rm "$zip_file" echo "Deleted $zip_file" else echo "Failed to extract $zip_file" fi done else echo "No zip files found." fiout_folder.sh放在下载目录，负责遍历移动子文件夹里面的内容到工作目录，文件内容如下：#!/bin/bash # 使用 find 命令查找所有文件并将它们移动到当前目录。 # -mindepth 2 确保我们只获取子文件夹中的文件，而不是当前目录中的文件。 # -maxdepth 2 确保我们不会深入到更深层次的子文件夹中。 # -type f 确保我们只处理文件，而不处理文件夹。 find . -mindepth 2 -maxdepth 2 -type f -exec mv {} . \; # 查找并删除空的子文件夹 # -empty 匹配空文件或空文件夹 # -type d 确保我们只处理文件夹 # -delete 删除匹配到的文件夹 find . -mindepth 1 -maxdepth 1 -type d -empty -delete echo "文件移动完成，并且空文件夹已被删除。"rename_songs.py负责重命名和整理歌曲相关文件，依据截取到的html进行排序整理。需要把网易云音乐网页版的专辑曲目部分的的html代码保存到同目录的content.txt文件中，执行后会自动解析保存的html代码并依据此进行重命名和排序。文件内容如下：import os from bs4 import BeautifulSoup import re import unicodedata def sanitize_title(title): """清理标题以进行精确匹配，保留非ASCII字符""" # 只移除ASCII范围内的非字母数字字符，并转换为小写 return ''.join([c for c in unicodedata.normalize('NFKC', title) if c.isalnum() or not (0 <= ord(c) < 128)]).lower() def clean_title(title): """清理并标准化歌曲标题""" # 将HTML实体替换为空格 title = re.sub(r' ', ' ', title) # 移除多余的空白字符，但保留单词间的单个空格 title = re.sub(r'\s+', ' ', title).strip() # 去除首尾可能出现的特殊字符或符号 title = re.sub(r'^[^\w\s]+|[^\w\s]+$', '', title) # 统一下划线和空格 title = re.sub(r'_', ' ', title) return title def parse_html(html_content): soup = BeautifulSoup(html_content, 'html.parser') # 查找所有的表格行，仅限于 中的行 rows = soup.select('tbody tr') songs = [] for row in rows: # 获取编号 num_tag = row.find('span', class_='num') number = num_tag.get_text(strip=True) if num_tag else 'N/A' # 尝试获取歌曲标题，考虑了可能存在的多种情况 title_tag = row.select_one('td:nth-of-type(2) b, td:nth-of-type(2) a') if title_tag: # 清理标题文本，移除干扰元素 for unwanted in title_tag.find_all('div', class_='soil'): unwanted.extract() # 移除不需要的 标签 title = clean_title(title_tag.get('title') or title_tag.get_text(strip=True)) sanitized_title = sanitize_title(title) else: title = 'Title Not Found' sanitized_title = sanitize_title(title) # 只有当number不是'N/A'且title不是'Title Not Found'时加入列表 if number != 'N/A' and title != 'Title Not Found': songs.append((number, title, sanitized_title)) print("Parsed Songs:") for song in songs: print(f"Number: {song[0]}, Title: {song[1]}, Sanitized Title: {song[2]}") return songs def standardize_filename(filename): """标准化文件名，将下划线替换为空格，并清理多余空格""" base_name, ext = os.path.splitext(filename) standardized_base_name = re.sub(r'_', ' ', base_name) standardized_base_name = re.sub(r'\s+', ' ', standardized_base_name).strip() return f"{standardized_base_name}{ext}" def rename_files_in_directory(directory_path, songs): # 遍历目录中的所有文件 renamed_files = set() # 用于跟踪已经重命名的文件，避免重复重命名 for filename in os.listdir(directory_path): file_path = os.path.join(directory_path, filename) # 如果是文件并且文件名（不包括扩展名）与解析出的歌曲名称匹配，则重命名 if os.path.isfile(file_path) and not filename.endswith('.txt') and not filename.endswith('.py'): standardized_filename = standardize_filename(filename) base_name, ext = os.path.splitext(standardized_filename) matched = False for i, (number, title, sanitized_title) in enumerate(songs, start=1): sanitized_base_name = sanitize_title(base_name) # 使用清理后的字符串进行精确匹配 if sanitized_base_name == sanitized_title: # 格式化编号为两位数 formatted_number = f"{i:02d}" new_filename = f"{formatted_number}. {base_name}{ext}" # 使用标准化后的文件名作为歌曲名 new_file_path = os.path.join(directory_path, new_filename) if not os.path.exists(new_file_path): # 检查新文件名是否已存在 if filename not in renamed_files: print(f'Renaming "{filename}" to "{new_filename}"...') try: os.rename(file_path, new_file_path) renamed_files.add(filename) # 添加到已重命名集合中 matched = True except Exception as e: print(f"Error renaming '{filename}': {e}") else: print(f"New filename '{new_filename}' already exists.") break # 文件名匹配成功后跳出循环，避免重复重命名 if not matched: print(f"No match found for file '{filename}'.") def handle_jpg_files(directory_path): """处理目录中的所有 .jpg 文件，保留一个并重命名为 cover.jpg，删除其余的""" jpg_files = [f for f in os.listdir(directory_path) if f.endswith('.jpg')] if len(jpg_files) > 0: # 选择第一个找到的 .jpg 文件作为封面 cover_file = jpg_files[0] cover_path = os.path.join(directory_path, cover_file) new_cover_path = os.path.join(directory_path, 'cover.jpg') # 如果cover.jpg不存在，则重命名第一个找到的.jpg文件为cover.jpg if not os.path.exists(new_cover_path): os.rename(cover_path, new_cover_path) print(f'Renamed "{cover_file}" to "cover.jpg".') # 删除其他的 .jpg 文件 for jpg in jpg_files[1:]: jpg_path = os.path.join(directory_path, jpg) os.remove(jpg_path) print(f'Deleted "{jpg}".') else: print("No .jpg files found to process.") if __name__ == "__main__": # 获取当前脚本所在的目录 directory_path = os.path.dirname(os.path.abspath(__file__)) # 处理 .jpg 文件 handle_jpg_files(directory_path) # 读取同目录下的 content.txt 文件获取 HTML 内容 content_file_path = os.path.join(directory_path, 'content.txt') try: with open(content_file_path, 'r', encoding='utf-8') as file: html_content = file.read() except FileNotFoundError: print("Error: The file 'content.txt' was not found in the current directory.") exit(1) except Exception as e: print(f"An error occurred while reading 'content.txt': {e}") exit(1) songs = parse_html(html_content) rename_files_in_directory(directory_path, songs) print("All processes completed.")至于怎么获取专辑曲目部分的的html代码，直接在浏览器右键歌曲列表，检查代码，一层一层往上找，当显示是整个列表的内容的元素，那就找到了。然后，右键，复制内部html，这就是需要的东西了。把这些html代码粘贴到content.txt就行了。注意，上述的四个文件：open_zip.sh，out_folder.sh，rename_songs.py，content.txt都是放在专辑的下载目录下。以上准备工作做完之后，在当前目录打开命令行，就可以施展组合拳了：./open_zip.sh ./out_folder.sh python rename_songs.py什么？Python提示ModuleNotFoundError: No module named 'xxx'?缺模块就去装啊，自己去搜，看了资料还不会装的话，只能说你唐！唐完了！！！如果一切顺利，目录就会变成上面示例目录结构专辑文件夹里面的样子了。小福利：Taylor Swift 的专辑为了更直观的看到是怎么组织文件目录的，我就把我整理的Taylor Swift的几个专辑分享出来吧！分别是：《Red》《1989》《Fearless》《Lover》《Speak Now》《Taylor Swift》以下是压缩分卷，解压密码是GTX1080Ti：Taylor Swift.part1.rar Taylor Swift.part5.rar Taylor Swift.part4.rar Taylor Swift.part3.rar Taylor Swift.part2.rar 别告诉我你不会解压缩，不然我只会骂你唐货！2025年12月补充：新文章有更好的音乐爬取方法，详情看用 Python 破解网易云 iframe 反爬，全自动构建本地音乐库 这篇文章
修复联想整机主板不认Linux EFI引导唐氏联想主板BIOS今天我手贱，倒腾了一下家里的联想台式机。结果发现再次开机显示：Operating System not found我直接蒙逼——系统盘我啥都没动！后面折腾了好多次，还是不行。我觉得不应该，于是拿来了另一个主板。好家伙，直接引导进系统了！联想，我日你仙人！我也是第一次遇到这种问题，来来回回折腾了一个晚上，好在后面解决了。记录一下。修复主板引导进入Linux Live环境首先要准备一个能启动Linux live 环境的镜像，把他丢到Ventoy启动盘里面。这个镜像最好是和被修复系统是同一个发行版，我这里是AlmaLinux。然后开机的时候，选择启动设备，选择Ventoy启动盘，进入Linux的Live环境。执行修复修复命令如下：efibootmgr -c -d /dev/sdb -p 1 -L "AlmaLinux" -l \\EFI\\almalinux\\grubx64.efi/dev/sdb替换成live环境下你系统盘的设备文件。-p 1中的1是EFI分区编号，是第几分区。-L "AlmaLinux"中，AlmaLinux是启动项的名称。-l \\EFI\\almalinux\\grubx64.efi这个是指定efi文件路径。完事后，重启，应该就能引导系统了。
80包邮的显卡手办这张GTX780Ti是我花了80包邮盘下来的。卖家说成色不错，但是是坏卡，点不亮。因为不是很像拿那张好的卡做摆件，怕把玩坏（主要是静电），正好遇到便宜的坏卡，就买了。到手外观到手后感觉橙色确实可以，原灰未动。就是散热器的散热片颜色不太对，按理来说应该是黑色的，感觉应该是装了GTX780的散热片。两张GTX780Ti白色散热器那张是新买的，还没清灰，有点脏。拆解显卡拆开了，全是渗油的导热垫感觉PCB的橙色确实可以，目前不能确定是什么问题，打算放假回去让我朋友帮看看，如果能修就修，救不了就安心做摆件了。沾满硅油的中框看得出来，GTX780Ti确实是大核弹，发热巨大。拆下来的涡轮风扇特写其实这玩意正确的叫法是离心风扇这玩意是最难清理的，如果想要清理的干干净净的话，扇叶很难清理，比轴流风扇难多了。清理后的PCB感觉还是很干净的，氧化腐蚀的痕迹并不明显。整体PCB橙色比我另一张卡好太多了。全部零件合影我感觉如果搞个展示框，裱起来，应该是挺好看的。上机“点亮”（迫真）反正是坏卡，上机后，灯亮了，风扇转，卡自检卡比尔。就那样吧。
收到礼物的原因2024年12月低，我为了白嫖华为云代金券参加了华为云的在线课程。完成了两个任务，白嫖了200的代金券。其中一个任务还可以申请领取水杯。于是我就申请了，没想到华为云居然掐着点，2025年1月1日送到了。礼物展示就是一个蓝色的水杯，虽然说不上特别而好看，但是我感觉还行。礼轻情意重 白送的不要白不要