在学习 Golang 时,我发现它在语法上既有类似 C++ 和 Python 的地方,也有独特之处。与 C++ 一样,Golang 是强类型的、编译型语言,强调性能和效率,但它去除了头文件和类等复杂结构,更简洁高效。而与 Python 相似,Golang 提供了自动垃圾回收、内置的并发支持以及简洁的代码风格,减少了编码的繁琐。Golang 没有 Python 的动态类型,变量声明严格要求类型,但比 C++ 更灵活,支持类型推断。整体上,Golang 更注重简洁性和工程化,介于 C++ 的复杂和 Python 的灵活之间,特别适合并发编程和大规模应用开发。
此文章虽名为 Go 语言入门指南:基础语法和常用特性解析,实际上也是是我本人的学习笔记、学习记录、复习资料。
0x00-hello:Go 语言入门之 Hello World
学习一个新语言编写的第一个程序一定是 Hello World 。(严肃
Golang 的语法与 C++、Python 非常相似,所以语法入门很轻松(其他语言没仔细学过,不清楚是否相似
(感觉变量声明和 JS 挺像, var
关键概念
- 包声明(Package Declaration) :
package main表示这是一个独立可执行的程序,Go 程序的执行入口必须在main包中。 - 导入语句(Import Statement) :
import "fmt"导入了 Go 的标准库fmt,用于格式化输入和输出。 - 主函数(Main Function) :
func main()定义了程序的入口函数main,所有可执行程序都从这里开始运行。 - 输出函数(Print Function) :
fmt.Println("hello world")使用fmt包中的Println函数,在控制台输出指定的字符串并换行。
代码解析
package main
import (
"fmt"
)
func main() {
fmt.Println("hello world")
}
- 程序入口:
func main()是程序的主函数,所有代码的执行都从这里开始。 - 使用标准库:通过导入
fmt包,可以方便地使用格式化输出功能。 - 代码风格:Go 强制使用一致的代码格式,使得代码易读易维护。(Goland 用户在保存代码的时候应该能感受到
0x01-var:Go 语言变量与常量
接下来我们学习 Go 语言变量与常量的声明与类型转换
关键概念
- 变量声明:使用
var关键字可以声明变量,支持类型推断和多变量同时声明。(感觉和 JS 类似 - 短变量声明:使用
:=可以进行简洁的变量声明和赋值操作。 - 常量定义:使用
const关键字定义常量,常量的值在编译时确定,不能被修改。 - 类型转换:需要显式地进行类型转换,特别是在涉及不同精度的浮点数时。
- 数学运算:通过标准库
math可以进行数学计算,例如正弦函数math.Sin。
代码解析
package main
import (
"fmt"
"math"
)
func main() {
var a = "initial" // 声明字符串变量,类型由编译器推断
var b, c int = 1, 2 // 同时声明多个整数变量并初始化
var d = true // 声明布尔变量,类型推断为 bool
var e float64 // 声明浮点数变量,默认值为 0
f := float32(e) // 将 float64 类型的 e 转换为 float32 类型
g := a + "foo" // 字符串拼接,使用短变量声明
fmt.Println(a, b, c, d, e, f) // 输出变量的值:initial 1 2 true 0 0
fmt.Println(g) // 输出拼接后的字符串:initialfoo
const s string = "constant" // 定义字符串常量
const h = 500000000 // 定义整数常量
const i = 3e20 / h // 科学计数法表示的大数运算,结果为 float64
fmt.Println(s, h, i, math.Sin(h), math.Sin(i)) // 输出常量的值和计算结果
}
- 变量的多种声明方式:可以使用
var关键字显式声明变量,也可以使用:=进行隐式声明并初始化。 - 类型推断:当声明变量时,如果未指定类型,Go 会根据初始化值自动推断类型。
- 默认零值:未初始化的变量会有一个默认的零值,例如数值类型为
0,布尔类型为false,字符串为""。 - 类型转换的重要性:在需要将一种类型转换为另一种类型时,必须显式地进行转换,例如将
float64转换为float32。 - 常量的特点:常量在程序运行期间不可改变,且可以参与编译期计算。
- 使用标准库进行数学计算:通过
math包,可以调用数学函数,如math.Sin计算正弦值。
0x02-for:Go 语言中的 for 循环
接下来,我们学习 Golang 的循环结构。与 C++ 相同,它可以使用 break 和 continue 来控制循环流程。
关键概念
-
无限循环:使用
for {}创建一个没有条件的循环,需要配合break语句来终止。 -
条件循环:
for后跟一个条件表达式,循环会在条件为true时继续。 -
计数循环:
for循环包含初始化语句、条件表达式和后置语句,类似于其他语言中的for循环。 -
控制语句:
break:立即终止循环的执行。continue:跳过本次循环的剩余代码,直接进入下一次迭代。
代码解析
package main
import "fmt"
func main() {
i := 1
for {
fmt.Println("loop")
break
}
for j := 7; j < 9; j++ {
fmt.Println(j)
}
for n := 0; n < 5; n++ {
if n%2 == 0 {
continue
}
fmt.Println(n)
}
for i <= 3 {
fmt.Println(i)
i = i + 1
}
}
-
无限循环与
break:for { ... }创建了一个无限循环,但在循环体内使用了break,所以只会执行一次,输出"loop"。
-
计数循环:
for j := 7; j < 9; j++ { ... }初始化了j = 7,当j < 9时循环,j每次递增 1。循环体内输出7和8。
-
使用
continue跳过迭代:for n := 0; n < 5; n++ { ... },在循环体内使用if n%2 == 0 { continue },当n为偶数时跳过,输出奇数1和3。
-
条件循环:
for i <= 3 { ... },当i小于等于3时循环,手动增加i的值,输出1,2,3。
0x03-if:Go 语言中的 if 语句
在这章,我们主要学习 Golang 的 选择结构。
关键概念
- 条件判断:使用
if、else if、else来控制程序的执行流程。 - 模运算符
%:用于计算两个整数相除的余数,常用于判断奇偶性或可整除性。 - 短变量声明:在
if语句中使用:=进行变量的声明,使变量的作用域限定在条件语句内部。
代码解析
package main
import "fmt"
func main() {
if 7%2 == 0 {
fmt.Println("7 is even")
} else {
fmt.Println("7 is odd")
}
if 8%4 == 0 {
fmt.Println("8 is divisible by 4")
}
if num := 9; num < 0 {
fmt.Println(num, "is negative")
} else if num < 10 {
fmt.Println(num, "has 1 digit")
} else {
fmt.Println(num, "has multiple digits")
}
}
- 判断奇偶性:使用
7%2来获取 7 除以 2 的余数,如果余数为 0,则说明是偶数,否则是奇数。(不知道是否可以像 C++ 里一样使用异或操作来判断?实测可以,但是判断条件不会隐式转换为bool 类型,Golang 中有 bool 类型吗?所以需要写等式:7&1 == 0(有点怀念 C++ 的隐式转换了 - 判断可整除性:通过
8%4 == 0来检查 8 是否能被 4 整除。 - 条件语句中的变量声明:在
if语句中使用num := 9,这样num的作用域仅限于当前的if-else块,提高了变量管理的局部性。 - 多重条件判断:使用
else if来增加额外的条件判断,例如判断一个数是负数、只有一位数还是多位数。
0x04-switch:Go 语言中的 switch 语句
是那个 Switch 吗?
感觉比 C++ 的 switch-case 更好用了
关键概念
- switch 语句:用于执行多分支选择,比起多个
if-else结构,switch语句使代码更加清晰和简洁。 - 多条件匹配:在
case语句中,可以使用逗号分隔多个匹配条件,实现对多个值的统一处理。 - 无条件 switch:
switch后不跟表达式,直接在case中编写条件判断,类似于多个if-else结构,但更加紧凑。 - time 包:Go 的标准库,提供了时间的获取和格式化功能,例如
time.Now()获取当前时间。
代码解析
package main
import (
"fmt"
"time"
)
func main() {
a := 2
switch a {
case 1:
fmt.Println("one")
case 2:
fmt.Println("two")
case 3:
fmt.Println("three")
case 4, 5:
fmt.Println("four or five")
default:
fmt.Println("other")
}
t := time.Now()
switch {
case t.Hour() < 12:
fmt.Println("It's before noon")
default:
fmt.Println("It's after noon")
}
}
在这段代码中,有两个主要部分:
-
基本的 switch 用法:
- 定义了变量
a := 2。 - 使用
switch a对变量a的值进行匹配。 case语句分别匹配1,2,3,以及4, 5(多个值匹配同一代码块)。- 当
a等于2时,匹配到case 2,输出"two"。 default语句用于在没有匹配到任何case时执行相应的代码。
- 定义了变量
-
无条件的 switch 用法:
- 使用
t := time.Now()获取当前时间。 switch后没有指定变量或表达式,直接在case中编写条件。case t.Hour() < 12判断当前时间是否在中午之前。- 根据条件,输出
"It's before noon"或"It's after noon"。
- 使用
0x05-array:Go语言中的数组
学了循环结构,当然要学习数组啦!!
关键概念
- 数组的声明和初始化:如何声明固定长度的数组,以及给数组赋值。
- 数组的长度:使用内置函数
len()获取数组的长度。 - 多维数组:理解如何声明和操作多维数组。
- 数组的遍历:使用循环对数组进行遍历和赋值。
代码解析
package main
import "fmt"
func main() {
var a [5]int
a[4] = 100
fmt.Println("get:", a[2])
fmt.Println("len:", len(a))
b := [5]int{1, 2, 3, 4, 5}
fmt.Println(b)
var twoD [2][3]int
for i := 0; i < 2; i++ {
for j := 0; j < 3; j++ {
twoD[i][j] = i + j
}
}
fmt.Println("2d: ", twoD)
}
- 数组的声明:使用
var a [5]int可以声明一个长度为5的整数数组a,默认情况下,数组中的元素都会被初始化为零值。 - 数组元素的赋值和访问:可以通过索引来赋值或访问数组的元素。例如,
a[4] = 100将第5个元素设置为100;a[2]可以访问第3个元素。 - 获取数组长度:使用
len(a)函数可以获取数组a的长度,这对于遍历数组非常有用。 - 数组的初始化:可以在声明时直接初始化数组,例如
b := [5]int{1, 2, 3, 4, 5},这样b数组就包含了指定的值。 - 打印数组:使用
fmt.Println()可以直接打印整个数组的内容,例如fmt.Println(b)会输出数组b的所有元素。 - 多维数组的声明和操作:
var twoD [2][3]int声明了一个二维数组,其大小为2x3。通过嵌套的for循环,可以对多维数组进行遍历和赋值。twoD[i][j] = i + j将计算结果赋值给二维数组中的每个元素。 - 遍历多维数组并赋值:使用嵌套循环,可以高效地遍历多维数组的每一个元素,这对于处理矩阵或表格数据非常有用。
注: 第一次看见
fmt.Println("2d: ", twoD)真的很吃惊,居然能直接输出二维数组
0x06-Slice:Go 语言中的切片(Slice)应用
切片(Slice)在 Python 中可是很常用哦~
这里的语法和 Python 的很像
关键概念
- 切片(Slice) : 切片是 Go 语言中对数组的一个轻量级封装,提供了更灵活和强大的操作方式。与数组不同,切片的长度可以动态变化。
make函数: 用于创建切片、映射和通道。make([]string, 3)创建了一个长度为3的字符串切片。append函数: 用于向切片中追加元素。它会根据需要自动扩展切片的容量。copy函数: 用于复制一个切片到另一个切片。copy(c, s)将切片s的内容复制到切片c中。- 切片操作(Slicing) : 通过指定索引范围,可以从一个切片中创建一个新的切片。例如,
s[2:5]获取索引2到4的元素。 - 切片字面量(Slice Literals) : 直接初始化切片的一种方式,如
good := []string{"g", "o", "o", "d"}。
代码解析
package main
import "fmt"
func main() {
s := make([]string, 3)
s[0] = "a"
s[1] = "b"
s[2] = "c"
fmt.Println("get:", s[2]) // c
fmt.Println("len:", len(s)) // 3
s = append(s, "d")
s = append(s, "e", "f")
fmt.Println(s) // [a b c d e f]
c := make([]string, len(s))
copy(c, s)
fmt.Println(c) // [a b c d e f]
fmt.Println(s[2:5]) // [c d e]
fmt.Println(s[:5]) // [a b c d e]
fmt.Println(s[2:]) // [c d e f]
good := []string{"g", "o", "o", "d"}
fmt.Println(good) // [g o o d]
}
- 切片的创建与初始化:通过
make函数创建了一个长度为3的切片s,并依次赋值。使用切片字面量直接初始化切片good。 - 动态扩展切片:使用
append函数向切片s中追加元素,不仅可以一次追加一个元素,也可以一次性追加多个元素。这展示了切片在处理动态数据时的灵活性。 - 复制切片:通过
copy函数将切片s的内容复制到新切片c中。 - 切片的切片操作:通过索引范围获取切片的子集,如
s[2:5]获取第3到第5个元素,s[:5]获取前5个元素,以及s[2:]获取从第3个元素到最后的所有元素。 - 切片的长度与访问:通过
len(s)获取切片的长度。
0x07-Map:Go 语言中的 Map 数据结构
map 就是哈希映射,类似 C++ STL 中的 map 。(不知道底层是否也是红黑树实现的呢
关键概念
-
Map 数据结构:
map是 Go 语言中用于存储键值对的数据结构,类似于其他语言中的字典或哈希表。
-
创建 Map:
- 使用
make函数或字面量语法来创建map。 - 例如,
m := make(map[string]int)创建了一个键为字符串类型,值为整数类型的map。
- 使用
-
添加和更新元素:
- 通过
m["key"] = value的方式添加或更新map中的元素。
- 通过
-
查询元素:
- 使用
m["key"]可以获取对应键的值。如果键不存在,返回值类型的零值。 - 使用
r, ok := m["key"]可以同时获取值和键是否存在的布尔值。
- 使用
-
删除元素:
- 使用
delete(m, "key")可以从map中删除指定键的元素。
- 使用
-
Map 的长度:
len(m)返回map中键值对的数量。
代码解析
package main
import "fmt"
func main() {
m := make(map[string]int)
m["one"] = 1
m["two"] = 2
fmt.Println(m) // map[one:1 two:2]
fmt.Println(len(m)) // 2
fmt.Println(m["one"]) // 1
fmt.Println(m["unknow"]) // 0
r, ok := m["unknow"]
fmt.Println(r, ok) // 0 false
delete(m, "one")
m2 := map[string]int{"one": 1, "two": 2}
var m3 = map[string]int{"one": 1, "two": 2}
fmt.Println(m2, m3)
}
0x08:Go 语言中的 range 关键字使用
类似 C++ 中 range::sort(array) 里的意思,就是遍历数组全部
关键概念
-
range关键字:- 用于在循环中遍历切片、数组、映射、字符串等数据结构。
- 在遍历切片或数组时,
range返回索引和值。 - 在遍历映射时,
range返回键和值。
代码解析
package main
import "fmt"
func main() {
nums := []int{2, 3, 4}
sum := 0
for i, num := range nums {
sum += num
if num == 2 {
fmt.Println("index:", i, "num:", num) // index: 0 num: 2
}
}
fmt.Println(sum) // 9
m := map[string]string{"a": "A", "b": "B"}
for k, v := range m {
fmt.Println(k, v) // b B; a A
}
for k := range m {
fmt.Println("key", k) // key a; key b
}
}
在这段代码中:
-
遍历切片
nums:- 使用
for i, num := range nums循环遍历切片,每次迭代获取当前元素的索引i和值num。 - 累加
sum变量以计算切片中所有元素的总和。 - 当
num等于 2 时,打印出当前元素的索引和值。
- 使用
-
输出总和:
- 循环结束后,打印出
sum的值,结果为 9。
- 循环结束后,打印出
-
遍历映射
m:- 使用
for k, v := range m循环遍历映射,获取每个键k和对应的值v,并打印出来。 - 由于映射是无序的,输出的顺序可能不固定,例如
b B和a A。
- 使用
-
仅遍历映射的键:
- 使用
for k := range m循环仅获取映射中的键k,并打印出来。
- 使用
0x09-func:Go 语言函数应用
准备迎接嵌套和递归吧~
关键概念
-
函数定义(Function Definition) :
- 展示了如何定义函数,包括不同的参数声明方式。
-
参数声明优化(Optimized Parameter Declaration) :
- 使用简化的参数声明方式,如
func add2(a, b int) int,提高代码的简洁性。
- 使用简化的参数声明方式,如
-
多返回值(Multiple Return Values) :
exists函数返回两个值,展示了 Go 语言支持函数返回多个值的特性。
-
映射(Map)操作:
- 在
exists函数中,演示了如何从 map 中检索值及其存在性。
- 在
-
函数调用与结果处理(Function Invocation and Result Handling) :
- 在
main函数中,展示了如何调用自定义函数并处理返回结果。
- 在
代码解析
package main
import "fmt"
func add(a int, b int) int {
return a + b
}
func add2(a, b int) int {
return a + b
}
func exists(m map[string]string, k string) (v string, ok bool) {
v, ok = m[k]
return v, ok
}
func main() {
res := add(1, 2)
fmt.Println(res) // 3
v, ok := exists(map[string]string{"a": "A"}, "a")
fmt.Println(v, ok) // A True
}
- 简洁写法:在这段代码中,
add和add2函数展示了两种不同的参数声明方式,add2使用了参数类型的简化写法,使代码更加简洁。 - 多返回值特性:
exists函数则展示了如何从 map 中检索一个键对应的值及其存在性,返回了两个值,这体现了 Go 语言函数的多返回值特性。
0x10-point:Go 语言函数与指针应用
形参和实参得分清楚哦!
与 C++ 的语法基本相似
关键概念
-
值传递(Pass by Value) :
- 在函数
add2中,参数n是通过值传递的。这意味着函数内部对n的修改不会影响到函数外部的变量。
- 在函数
-
指针传递(Pass by Pointer) :
- 函数
add2ptr接受一个整数指针int作为参数。通过指针,可以直接修改原变量的值。
- 函数
-
指针操作(Pointer Operations) :
- 使用
&n获取变量n的内存地址,并将其传递给add2ptr。 - 在
add2ptr函数内部,使用n解引用指针,从而修改n所指向的实际值。
- 使用
-
变量声明与初始化:
- 使用简短声明
n := 5初始化变量n为5。
- 使用简短声明
代码解析
package main
import "fmt"
func add2(n int) {
n += 2
}
func add2ptr(n *int) {
*n += 2
}
func main() {
n := 5
add2(n)
fmt.Println(n) // 5
add2ptr(&n)
fmt.Println(n) // 7
}
在这段代码中,add2 函数通过值传递接收参数 n,在函数内部对 n 进行加2操作,但这种修改仅限于函数内部,不会影响到 main 函数中的变量 n,因此第一次输出仍为5。而 add2ptr 函数通过指针传递接收参数 n,使用解引用操作 *n 直接修改了 main 函数中变量 n 的值,使其增加到7。
0x11-struct:Go 语言结构体与方法使用
通过创建不同的结构体实例来使用结构体字段。同时,程序展示了两种检查用户密码的方法:一种通过值传递,另一种通过指针传递。
关键概念
-
结构体(Struct) :
type user struct定义了一个user结构体,包含两个字段:name和password。结构体是 Go 中一种自定义的数据类型,适用于描述具有多个字段的对象,比如用户信息。
-
结构体实例化:
- 通过
user{...}语法可以创建结构体实例,支持多种初始化方式:完整字段赋值、部分字段赋值,甚至初始化后单独赋值。
- 通过
-
值传递与指针传递:
- 函数
checkPassword使用值传递,将user结构体的副本传入函数。 checkPassword2使用指针传递,将user的地址传入函数。这种方式可以避免在调用时复制整个结构体,适合较大结构体或需要在函数中修改原始数据的情况。
- 函数
代码解析
package main
import "fmt"
type user struct {
name string
password string
}
func main() {
a := user{name: "wang", password: "1024"} // 完整字段赋值
b := user{"wang", "1024"} // 省略字段名
c := user{name: "wang"} // 部分字段赋值
c.password = "1024" // 单独赋值
var d user // 声明未初始化
d.name = "wang"
d.password = "1024"
fmt.Println(a, b, c, d) // {wang 1024} {wang 1024} {wang 1024} {wang 1024}
fmt.Println(checkPassword(a, "haha")) // false
fmt.Println(checkPassword2(&a, "haha")) // false
}
func checkPassword(u user, password string) bool {
return u.password == password
}
func checkPassword2(u *user, password string) bool {
return u.password == password
}
-
结构体
user定义了name和password两个字段。 -
主函数中,展示了四种创建和初始化结构体实例的方法:
- 使用字段名完整赋值。
- 省略字段名(按顺序赋值)。
- 部分字段赋值后,再单独设置字段值。
- 声明结构体变量后,手动为字段赋值。
-
checkPassword和checkPassword2是用于检查密码的两个函数。第一个函数接受user结构体的值,第二个函数接受指向user的指针。
0x12-struct_method:Go 语言中的结构体方法
类似 类(Class)的概念~
通过定义一个 user 结构体并实现与其相关的方法,程序能够检查和重置用户密码。展示了 Go 语言面向对象编程的一部分,通过结构体和方法实现对数据的封装和操作。
关键概念
-
结构体(Struct) :
type user struct定义了一个名为user的结构体,包含两个字段:name和password。结构体是 Go 语言中用于组合数据的基本类型,可以看作是自定义的数据类型。
-
方法(Method) :
- 方法是与特定类型关联的函数。在本例中,
checkPassword和resetPassword是两个方法,分别用于检查密码和重置密码。通过这种方式,能够在逻辑上将数据和操作封装在一起。
- 方法是与特定类型关联的函数。在本例中,
-
值接收者与指针接收者(Value Receiver vs. Pointer Receiver) :
checkPassword方法使用值接收者,这意味着在调用该方法时,结构体的一个副本被传递。相对而言,resetPassword方法使用指针接收者,因此在方法中对密码的更改会影响原始结构体。这种设计允许在方法内修改结构体字段。
代码解析
package main
import "fmt"
type user struct {
name string
password string
}
func (u user) checkPassword(password string) bool {
return u.password == password
}
func (u *user) resetPassword(password string) {
u.password = password
}
func main() {
a := user{name: "wang", password: "1024"}
a.resetPassword("2048")
fmt.Println(a.checkPassword("2048")) // true
}
结构体 user 代表用户信息,包含用户名和密码字段。通过 checkPassword 方法,可以检查输入的密码是否与存储的密码匹配,而 resetPassword 方法则提供了更新用户密码的功能。
在 main 函数中,创建了一个 user 类型的实例 a,并初始化用户名和密码。随后,调用 resetPassword 方法将密码更新为 "2048",并使用 checkPassword 验证新的密码。最终,程序输出 true,表明密码重置成功,且检查结果符合预期。
0x13-error:Go 语言中的错误处理与查找用户示例
优雅的错误信息抛出方式~
关键概念
-
错误处理:
- Go 语言推荐使用
error接口类型来处理错误,而不是异常机制。代码中的errors.New()用于创建自定义错误信息,便于程序在特定条件不满足时返回错误。
- Go 语言推荐使用
-
指针与值传递:
findUser函数返回用户的指针(user)。通过指针传递用户数据,避免复制数据,同时节省内存,特别是当用户结构体较大时。
-
短变量声明与条件语句:
if u, err := findUser(...); err != nil { ... }是 Go 中典型的短变量声明方式,允许在一个条件表达式内定义并检查多个返回值。这样既精简了代码,又增强了可读性。
代码解析
package main
import (
"errors"
"fmt"
)
type user struct {
name string
password string
}
func findUser(users []user, name string) (v *user, err error) {
for _, u := range users {
if u.name == name {
return &u, nil
}
}
return nil, errors.New("not found")
}
func main() {
u, err := findUser([]user{{"wang", "1024"}}, "wang")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(u.name) // 输出:wang
if u, err := findUser([]user{{"wang", "1024"}}, "li"); err != nil {
fmt.Println(err) // 输出:not found
return
} else {
fmt.Println(u.name)
}
}
-
type user struct:定义了一个user结构体,包含用户名和密码字段,用于模拟用户信息。 -
findUser函数:该函数遍历用户列表,匹配用户名是否存在。如果找到,则返回用户指针和nil,否则返回nil和错误信息not found。 -
main函数:- 通过调用
findUser查找用户,利用if err != nil来判断是否有错误发生。这种判断机制可以让程序根据不同情况进行不同的操作,提高了代码的鲁棒性。 - 第二次调用
findUser使用了短变量声明和条件语句的组合形式 (if u, err := ...; err != nil { ... } else { ... }),这样可以一次性检查和处理返回值,避免多层嵌套结构,代码更加简洁。
- 通过调用
使用 errors.New() 创建自定义错误真的很方便!!
0x14-string:Go 语言字符串操作
字符串,可以玩马拉车啦~
关键概念
-
字符串不可变性:在 Go 语言中,字符串是不可变的,即一旦创建,字符串中的字符就不能被更改。如果需要修改字符串,实际上是生成了一个新的字符串。
-
UTF-8 字符串长度:
len()函数返回的是字符串的字节长度,而不是字符数。对于包含多字节字符(如中文)的字符串,字节数和字符数是不一样的。例如,"你好"的字节长度是 6,因为每个汉字占用 3 个字节。 -
strings包的字符串操作函数:- 查找子串、计数、前后缀判断、索引等都可以通过
strings包提供的函数实现,这些操作广泛用于字符串处理。
- 查找子串、计数、前后缀判断、索引等都可以通过
代码解析
package main
import (
"fmt"
"strings"
)
func main() {
a := "hello"
fmt.Println(strings.Contains(a, "ll")) // true
fmt.Println(strings.Count(a, "l")) // 2
fmt.Println(strings.HasPrefix(a, "he")) // true
fmt.Println(strings.HasSuffix(a, "llo")) // true
fmt.Println(strings.Index(a, "ll")) // 2
fmt.Println(strings.Join([]string{"he", "llo"}, "-")) // he-llo
fmt.Println(strings.Repeat(a, 2)) // hellohello
fmt.Println(strings.Replace(a, "e", "E", -1)) // hEllo
fmt.Println(strings.Split("a-b-c", "-")) // [a b c]
fmt.Println(strings.ToLower(a)) // hello
fmt.Println(strings.ToUpper(a)) // HELLO
fmt.Println(len(a)) // 5
b := "你好"
fmt.Println(len(b)) // 6
}
-
字符串查找和判断:
strings.Contains(a, "ll"):判断子串"ll"是否在字符串a中。strings.HasPrefix(a, "he")和strings.HasSuffix(a, "llo"):判断字符串是否有指定的前缀或后缀。strings.Index(a, "ll"):查找子串在字符串中的索引位置,如果不存在则返回1。
-
字符串计数与替换:
strings.Count(a, "l"):计算子串"l"在a中出现的次数。strings.Replace(a, "e", "E", -1):将字符串中的"e"替换为"E",1表示替换所有匹配的子串。
-
字符串的分隔和连接:
strings.Join([]string{"he", "llo"}, "-"):将字符串切片按指定分隔符连接成一个新字符串。strings.Split("a-b-c", "-"):按分隔符"-"将字符串分割成切片。
-
字符串大小写转换:
strings.ToLower(a)和strings.ToUpper(a):将字符串全部转为小写或大写。
-
字符串重复:
strings.Repeat(a, 2):将字符串重复指定次数。
-
字符串长度:
len(a):返回字符串的字节长度。在英文字符串中等于字符数,但在多字节字符(如中文)中,这个长度代表的是字节数,因此"你好"返回6而不是2。
这些是很常用的字符串处理、判断函数。
0x15-fmt:Go 语言格式化输出(fmt 包)
格式化打印输出
关键概念
-
格式化输出函数:
fmt.Println:用于简单输出,支持多变量,自动添加空格分隔和换行。fmt.Printf:支持格式化输出,通过指定格式化标识符控制输出内容的格式。
-
格式化标识符:
%v:通用占位符,打印变量的值。%+v:打印结构体时会包含字段名。%#v:打印变量的完整语法表示,适合用于调试。%.2f:用于格式化浮点数,指定保留小数位数。
-
结构体格式化:
- 使用
fmt.Printf可以自定义结构体的输出格式,提供更多关于结构体字段的信息。
- 使用
代码解析
package main
import "fmt"
type point struct {
x, y int
}
func main() {
s := "hello"
n := 123
p := point{1, 2}
fmt.Println(s, n) // hello 123
fmt.Println(p) // {1 2}
fmt.Printf("s=%v\\n", s) // s=hello
fmt.Printf("n=%v\\n", n) // n=123
fmt.Printf("p=%v\\n", p) // p={1 2}
fmt.Printf("p=%+v\\n", p) // p={x:1 y:2}
fmt.Printf("p=%#v\\n", p) // p=main.point{x:1, y:2}
f := 3.141592653
fmt.Println(f) // 3.141592653
fmt.Printf("%.2f\\n", f) // 3.14
}
在这段代码中,我学到了如何使用 fmt 包进行不同的格式化输出。
- 基本输出:
fmt.Println(s, n)简单地输出了字符串和整数值,并自动添加空格和换行符,这是打印基础信息最简单的方式。 - 结构体输出:直接使用
fmt.Println(p)可以输出结构体的字段值;而通过fmt.Printf使用%+v标识符可以包含字段名,比如p={x:1 y:2}。而%#v则输出了结构体的完整定义信息p=main.point{x:1, y:2},便于调试时观察结构体类型和字段信息。 - 浮点数格式化:
fmt.Printf("%.2f\\n", f)控制浮点数的输出精度,让我可以只显示两位小数。这种格式化方式在处理需要特定精度的数值输出时非常实用,例如保留两位小数的金额或百分比。
0x16-json:Go 语言 JSON 序列化与反序列化
将自定义结构体序列化为 JSON 格式的字符串,以及如何将 JSON 字符串反序列化回结构体。
快速创建结构体:curlconverter
关键概念
-
结构体定义与 JSON 标签:
- 该程序定义了一个
userInfo结构体,其中包含Name、Age和Hobby字段。在Age字段上使用了json:"age"标签,指定该字段在 JSON 中的键名为"age"。(不特别指出就默认键值为结构体中变量名
- 该程序定义了一个
-
JSON 序列化(Marshaling) :
- 程序使用
json.Marshal()将userInfo结构体实例a转换为 JSON 格式的字节切片。json.MarshalIndent()提供了格式化输出,增加可读性。(json.MarshalIndent()超级好用!
- 程序使用
-
JSON 反序列化(Unmarshaling) :
- 使用
json.Unmarshal()可以将 JSON 字节切片转换为userInfo结构体的实例。
- 使用
代码解析
package main
import (
"encoding/json"
"fmt"
)
type userInfo struct {
Name string
Age int `json:"age"`
Hobby []string
}
func main() {
a := userInfo{Name: "wang", Age: 18, Hobby: []string{"Golang", "TypeScript"}}
buf, err := json.Marshal(a)
if err != nil {
panic(err)
}
fmt.Println(buf)
fmt.Println(string(buf))
buf, err = json.MarshalIndent(a, "", "\\t")
if err != nil {
panic(err)
}
fmt.Println(string(buf))
var b userInfo
err = json.Unmarshal(buf, &b)
if err != nil {
panic(err)
}
fmt.Printf("%#v\\n", b)
}
-
结构体定义:
userInfo结构体定义了用户信息的基本字段。通过在Age字段上加上json:"age"标签,控制了序列化后的 JSON 键名。(避免 Go 语言结构体成员的首字母大写规则导致与 JSON 字段名称不匹配的问题。 -
序列化与格式化:
- 使用
json.Marshal(a)可以将结构体序列化为 JSON 字节切片,然后通过string(buf)转换为字符串输出。 json.MarshalIndent(a, "", "\\t")实现了格式化输出,这在调试和阅读 JSON 数据时非常有用。
- 使用
-
反序列化:通过
json.Unmarshal(buf, &b)将 JSON 字节切片转回userInfo结构体的实例,并使用fmt.Printf观察结构体内容,验证反序列化是否成功。
0x17-time:Go 语言时间操作
深入理解 time 包在处理日期和时间方面的基本用法。
关键概念
-
获取当前时间:
time.Now()返回当前的时间信息,包括日期、时间、时区等。这个方法用于获取程序运行时的准确时间。
-
创建指定时间:
time.Date()允许创建一个自定义的时间实例,可以指定年、月、日、小时、分钟、秒等参数,是构建特定时间的有效方法。
-
时间格式化与解析:
Format和Parse是time包中两个关键函数,用于将时间格式化为特定字符串格式以及从特定格式的字符串解析时间。
-
计算时间差:
Sub用于计算两个时间之间的差值。Sub返回Duration类型,可以进一步转换为分钟或秒等单位,适用于分析时间间隔。
-
Unix 时间戳:
Unix()方法将时间转换为 Unix 时间戳(自 1970 年 1 月 1 日以来的秒数),便于存储和比较时间。
代码解析
package main
import (
"fmt"
"time"
)
func main() {
now := time.Now()
fmt.Println(now) // 打印当前时间
t := time.Date(2022, 3, 27, 1, 25, 36, 0, time.UTC)
t2 := time.Date(2022, 3, 27, 2, 30, 36, 0, time.UTC)
fmt.Println(t) // 创建并打印自定义时间
fmt.Println(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute()) // 获取时间的各个部分
fmt.Println(t.Format("2006-01-02 15:04:05")) // 将时间格式化为字符串
diff := t2.Sub(t)
fmt.Println(diff) // 计算并打印时间差
fmt.Println(diff.Minutes(), diff.Seconds()) // 转换时间差为分钟和秒
t3, err := time.Parse("2006-01-02 15:04:05", "2022-03-27 01:25:36")
if err != nil {
panic(err)
}
fmt.Println(t3 == t) // 比较解析出的时间和已有时间是否相等
fmt.Println(now.Unix()) // 获取当前时间的 Unix 时间戳
}
0x18-strconv:Go 语言 strconv 包的基本使用
Go 语言标准库中的 strconv 包,主要用于处理字符串和其他数据类型之间的转换。
关键概念
-
strconv 包:
strconv是 Go 的标准库包,专门用于字符串和基本数据类型(如整数、浮点数、布尔值)之间的转换。在数据处理中,经常需要将用户输入或文件中的字符串数据解析为特定类型,strconv提供了简便的方法。
-
Parse 系列函数:
strconv.ParseFloat:将字符串解析为浮点数,支持指定精度。strconv.ParseInt:将字符串解析为整数,支持指定进制和位数。strconv.Atoi:将字符串直接解析为整数,是ParseInt的简化版本,默认使用十进制。
-
错误处理:
- Go 语言的错误处理机制很明确:多数解析函数返回解析结果和错误值。在解析失败时,程序可以通过检查错误值来处理异常数据,确保代码的健壮性。
代码解析
package main
import (
"fmt"
"strconv"
)
func main() {
f, _ := strconv.ParseFloat("1.234", 64)
fmt.Println(f) // 1.234
n, _ := strconv.ParseInt("111", 10, 64)
fmt.Println(n) // 111
n, _ = strconv.ParseInt("0x1000", 0, 64)
fmt.Println(n) // 4096
n2, _ := strconv.Atoi("123")
fmt.Println(n2) // 123
n2, err := strconv.Atoi("AAA")
fmt.Println(n2, err) // 0 strconv.Atoi: parsing "AAA": invalid syntax
}
-
ParseFloat 使用:
strconv.ParseFloat("1.234", 64)用于将字符串 "1.234" 解析为浮点数,指定 64 表示使用 64 位精度。ParseFloat是处理浮点数的常用方法,适合需要高精度的计算场景。
-
ParseInt 支持多种进制:
strconv.ParseInt("111", 10, 64)表示将十进制字符串 "111" 解析为 64 位整数。strconv.ParseInt("0x1000", 0, 64)体现了自动识别进制的能力,其中0x1000是十六进制格式。ParseInt在第二个参数为 0 时会根据前缀自动识别进制,非常方便。
-
Atoi 的快捷用法:
strconv.Atoi("123")可以快速将字符串 "123" 解析为整数,但它默认只能处理十进制。对于简单整数解析,Atoi提供了更简洁的写法。- 尝试将非数字字符串 "AAA" 转换为整数时,
Atoi返回了错误信息。err值来判断转换是否成功并存储具体问题的描述。
0x19-env:Go 语言系统交互之环境变量与命令执行
Golang 还可以直接处理环境变量、读取命令行参数,并调用系统命令。
关键概念
-
命令行参数:
- Go 使用
os.Args获取命令行传递的参数。通过这个方式,我们可以让程序根据传入的参数执行不同操作。
- Go 使用
-
环境变量:
- 使用
os.Getenv可以读取当前的环境变量。环境变量用于在不同的环境中传递配置信息。 os.Setenv可以设置新的环境变量,便于在程序运行期间临时调整环境设置。
- 使用
-
执行系统命令:
- 通过
exec.Command可以调用系统命令。在此程序中,grep被用来查找文件中的内容。使用CombinedOutput可以获取命令的输出和错误信息。
- 通过
代码解析
package main
import (
"fmt"
"os"
"os/exec"
)
func main() {
// go run example/20-env/main.go a b c d
fmt.Println(os.Args) // 打印所有命令行参数
fmt.Println(os.Getenv("PATH")) // 打印当前 PATH 环境变量的值
fmt.Println(os.Setenv("AA", "BB")) // 设置环境变量 AA 的值为 BB
buf, err := exec.Command("grep", "127.0.0.1", "/etc/hosts").CombinedOutput()
if err != nil {
panic(err)
}
fmt.Println(string(buf)) // 打印 grep 命令的输出结果
}
- 命令行参数:通过
os.Args获取程序运行时传递的参数,这在控制程序行为时很有用。运行go run example/20-env/main.go a b c d时,会打印出所有参数,例如[.../main a b c d]。 - 环境变量读取与设置:使用
os.Getenv可以读取环境变量,比如PATH。而os.Setenv可以设置新的环境变量,比如设置AA=BB,但此变量仅在当前进程有效。学习到这一点帮助理解如何在 Go 中读取和动态配置环境。 - 执行系统命令:
exec.Command("grep", "127.0.0.1", "/etc/hosts")用于执行grep命令,查找指定内容,并获取其输出。CombinedOutput将输出结果存储在buf中,若命令执行失败则err将捕获错误。这一部分展示了如何在 Go 中调用系统命令并获取输出。
总结
至此,Golang 的基本常用语法大概就是这些。我也要准备继续学习 Go 语言的工程实践了。