Go 语言入门指南:基础语法和常用特性解析 | 青训营

210 阅读6分钟

Go语言简单介绍

作为一个从C++来学golang的小白,个人总结了一些C++与Go的区别:

  1. 并发支持:
  • C++在并发编程方面需要依赖于操作系统提供的线程库和同步原语,如std::thread和std::mutex。
  • Go在语言级别原生支持并发,通过goroutine和channel来实现并发编程,使得编写并发程序变得更简单和安全。
  1. 内存管理:
  • C++需要手动管理内存,开发者需要负责分配和释放内存,这可能导致内存泄漏和悬挂指针等问题。
  • Go具有自动垃圾回收(GC)功能,开发者不需要显式管理内存,Go运行时会自动处理内存回收。
  1. 应用场景:
  • C++通常用于系统级编程、游戏开发、图形图像处理和嵌入式系统等需要高性能和底层控制的领域。
  • Go主要用于构建高并发的网络服务、Web后端、云原生应用和分布式系统等领域。
  1. 语法和简洁性:
  • C++的语法相对复杂,支持多种编程范式,因此代码可能显得更冗长。
  • Go的设计简洁,舍弃了一些复杂的特性,使得代码更易读、易写。
  1. 语法特点:
  • C++是一种多范式编程语言,支持面向对象编程(OOP)和泛型编程等多种编程范式。它允许开发者使用类、继承、模板等特性。
  • Go是一种以并发为中心的编程语言,鼓励使用轻量级并发(goroutine)和通信(channel)来处理并发任务,但它没有传统的类和继承概念。

总的来说,C++适用于对性能有严格要求或需要底层控制的任务,而Go则更适合构建高并发的网络应用和分布式系统,让开发人员能够更快速、安全地处理并发任务。

Go如何运行

  1. 直接编译运行
go run test.go
  1. 先编译成exe再运行
go build test.go //生成test.exe
./test.exe

Go基本语法

  1. 打印输出
// 表示这是一个可独立执行的程序,而不是一个库。
package main

// 导入 fmt 包,它提供了格式化的输入和输出功能。
import (
    "fmt"
)

// func main() 是程序的主入口点,当程序运行时,将首先执行这个函数。
func main() {
    // 调用 fmt 包中的 Println 函数,用于在控制台输出。
    fmt.Println("hello world")
}

  • 注意:在go语言中, { 必须在函数名后面,不能换行
  1. 变量的使用
package main

import (
    "fmt"
    "math"
)
func main() {
    // 声明并初始化变量 a 为 "initial",Go会自动推断其类型为字符串。
    var a = "initial"
    
    // 声明并初始化变量 d 为布尔类型,并赋值为 true。
    var d = true

    // 声明并初始化变量 b 和 c 为整数,分别赋值为 1 和 2。
    var b, c int = 1, 2

    // 声明变量 e 为浮点数,默认值为 0。
    var e float64

    // 使用简短变量声明语法,将 e 的值转换为 float32,并赋值给变量 f。
    f := float32(e)

    // 使用字符串拼接,将变量 a 的值与 "foo" 拼接,并赋值给变量 g。
    g := a + "foo"
    
    // 使用简短变量声明语法,赋值为1
    h := 1

    // 使用 fmt.Println 函数打印输出变量的值,每个值之间用空格分隔。
    fmt.Println(a, b, c, d, e, f, w) // 输出:initial 1 2 true 0 0
    fmt.Println(g)                // 输出:initialfoo

    // 声明常量 s 为字符串,赋值为 "constant"。
    const s string = "constant"

    // 声明常量 h 为整数,赋值为 500000000。
    const h = 500000000

    // 声明常量 i 为浮点数,赋值为 3e20 / h。3e20 表示 3 乘以 10 的 20 次方,即 3 * 10^20。
    const i = 3e20 / h

    // 使用 fmt.Println 函数打印输出常量的值以及 math 包中的 Sin 函数对 h 和 i 的计算结果。
    fmt.Println(s, h, i, math.Sin(h), math.Sin(i))
}
  • 注意:在go语言中, 声明的变量必须要使用,否则会编译出错
  • var 关键字用于声明变量,而 := 是简短变量声明语法,用于声明并初始化变量。
  • const 关键字用于声明常量。常量在声明时必须初始化,而且在编译时其值是固定的,不能被修改。
  1. 判断
func main() {
    // 判断 7 是否为偶数,由于 7%2 的结果为 1,所以条件不成立,执行 else 块中的代码。
    if 7%2 == 0 {
            fmt.Println("7 is even")
    } else {
            fmt.Println("7 is odd")
    }

    // 声明 num 为 9,并进行多重条件判断。
    // 首先判断 num 是否小于 0,由于 num 为正数,该条件不成立。
    // 接着判断 num 是否小于 10,由于 num 等于 9,该条件成立,输出 "9 has 1 digit"。
    // 如果前面两个条件都不成立,则执行最后的 else 块。
    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")
    }
}

  1. 循环
func main() {
    // 无限循环,不带条件的循环会一直执行,除非遇到 break 语句跳出循环。
    for {
            fmt.Println("loop")
            // 使用 break 语句跳出当前循环,终止无限循环。
            break
    }

    // 使用 for 循环的常规形式,初始化 j 为 7,j 小于 9 时执行循环,每次循环后 j 自增 1。
    for j := 7; j < 9; j++ {
            fmt.Println(j)
    }

    // 初始化 n 为 0,n 小于 5 时执行循环,每次循环后 n 自增 1。
    // 使用 continue 语句跳过当前循环中的剩余语句,进入下一次循环。
    for n := 0; n < 5; n++ {
            if n%2 == 0 {
                    continue
            }
            fmt.Println(n)
    }
   
    // 使用 for 循环的另一种形式,类似于 while 循环,只有条件部分,并且没有初始化和迭代部分。
    i := 1
    for i <= 3 {
            fmt.Println(i)
            i = i + 1
    }
}

  • Go语言只有 for 循环一种循环结构,但它有多种使用方式。
  • Go中的 for 循环有三种形式:无限循环、常规循环、类似 while 的循环。
  • 无限循环通过 for { ... } 的形式实现,当循环体内满足跳出条件时,可以使用 break 语句跳出循环。使用 continue 关键字可以跳过循环体中剩余的语句,直接进行下一次循环迭代。
  • 常规循环使用 for 初始化; 条件; 迭代 { ... } 的形式,与其他语言的循环类似。
  • 类似 while 的循环使用 for 条件 { ... } 的形式,没有初始化和迭代部分,可以在循环体内手动控制循环条件和迭代步骤。
  1. switch
func main() {
    a := 2
    // 使用 switch 语句根据 a 的值进行不同的处理。
    switch a {
        case 1:
            fmt.Println("one")
        case 2:
            fmt.Println("two") // 输出:two
        case 3:
            fmt.Println("three")
        case 4, 5:
            fmt.Println("four or five")
        default:
            fmt.Println("other")
    }

    // 获取当前时间。
    t := time.Now()
    // 使用 switch 语句根据当前时间的小时部分进行不同的处理。
    switch {
        case t.Hour() < 12:
                fmt.Println("It's before noon") 
        default:
                fmt.Println("It's after noon")
    }
}
  • switch 后面可以跟一个表达式或者不跟任何表达式。如果不跟表达式,则会在满足条件的分支执行后直接退出,相当于 switch true
  • case 后面跟的是一个或多个条件,用于匹配表达式的值。如果匹配成功,将执行相应的代码块。
  • 可以在一个 case 中同时匹配多个值,用逗号分隔,如 case 4, 5:
  • 如果没有匹配的 case,则会执行 default 分支(可选的),用于处理没有匹配的情况。
  • Go语言中的 switch 语句自带 break 功能,一旦执行了匹配的 case 分支,会自动退出整个 switch 语句。
  • 如果希望继续执行后续的 case 分支,可以使用 fallthrough 关键字,但该功能并不常用,一般不推荐使用。
  1. 数组
func main() {
    // 声明一个长度为 5 的整数数组 a,所有元素都会被初始化为整数类型的零值(0)。
    var a [5]int
    // 在数组 a 中的第 5 个位置(索引为 4)赋值为 100。
    a[4] = 100
    // 打印数组 a 中索引为 2 的元素的值
    fmt.Println("get:", a[2])
    // 打印数组 a 的长度,输出:len: 5
    fmt.Println("len:", len(a))

    // 使用数组字面值的方式创建数组 b,长度为 5,并初始化为 {1, 2, 3, 4, 5}。
    b := [5]int{1, 2, 3, 4, 5}
    // 打印数组 b 的值,输出:[1 2 3 4 5]
    fmt.Println(b)
    
    // 声明一个二维整数数组 twoD,大小为 2x3。
    var twoD [2][3]int
    // 使用嵌套循环为二维数组 twoD 赋值。
    for i := 0; i < 2; i++ {
        for j := 0; j < 3; j++ {
            twoD[i][j] = i + j
        }
    }
    // 打印二维数组 twoD 的值,输出:2d:  [[0 1 2] [1 2 3]]
    fmt.Println("2d: ", twoD)
}
  • Go语言中的数组是固定长度的数据结构,一旦创建,其长度不可更改。
  • 数组的索引从0开始,最大索引为长度减1。
  • 可以使用 var 关键字声明一个数组,并通过索引赋值和取值。
  • 使用数组字面值的方式可以在声明数组的同时进行初始化。
  1. 切片
func main() {
    // 使用 make 函数创建一个长度为 3 的字符串切片 s,初始值为默认零值(空字符串)。
    s := make([]string, 3)
    // 给切片 s 的前 3 个元素赋值。
    s[0] = "a"
    s[1] = "b"
    s[2] = "c"
    // 打印切片 s 的索引为 2 的元素的值,输出:get: c
    fmt.Println("get:", s[2])
    // 打印切片 s 的长度,输出:len: 3
    fmt.Println("len:", len(s))
    
    // 使用 append 函数向切片 s 添加元素 "d"。
    s = append(s, "d")
    // 使用 append 函数向切片 s 后续添加两个元素 "e" 和 "f"。
    s = append(s, "e", "f")
    // 打印切片 s 的值,输出:[a b c d e f]
    fmt.Println(s)

    // 创建一个与切片 s 长度相同的新切片 c,并将 s 的元素复制到 c 中。
    c := make([]string, len(s))
    copy(c, s)

    // 使用切片操作取出 s 中索引为 [2:5) 的子切片。
    // 注意:切片操作不包含结束索引位置的元素,输出:[c d e]
    fmt.Println(s[2:5])
    // 使用切片操作取出 s 中索引为 [2:len(s)) 的子切片。
    // 输出:[c d e f]
    fmt.Println(s[2:])

    // 直接声明一个字符串切片 good,并初始化为 {"g", "o", "o", "d"}。
    good := []string{"g", "o", "o", "d"}
    fmt.Println(good) // 输出:[g o o d]
}
  • Go语言中的切片(Slice)是一个灵活的动态数组,它是对数组的一个连续片段的引用。切片的长度可以动态增长或缩小,是一种动态数据结构。切片不需要指定固定长度,可以根据需要自动调整。
  • 切片是对数组的一个连续片段的引用,是动态长度的动态数组。
  • 使用 make([]T, length, capacity) 函数可以创建一个长度为 length 的切片,容量为 capacity。容量表示切片可增长的上限,可以省略,省略时容量与长度相同。
  • 可以通过 slice[low:high] 来获取切片的子切片,取值范围为索引 lowhigh-1
  • 使用 append 函数可以在切片末尾添加新的元素,如果容量不足,切片会自动扩容。
  • 使用 copy(dst, src) 函数可以将一个切片的元素复制到另一个切片。

数组和切片区别:

  • 长度固定 vs. 长度动态:
    • 数组是一个长度固定的数据结构,在声明时需要指定其长度,并且不能动态增长或缩小。
    • 切片是一个长度动态的数据结构,它是对数组的一个连续片段的引用,并且可以动态增长或缩小。
  • 声明方式:
    • 数组的声明方式为 [长度]类型,如 [5]int 表示长度为 5 的整数数组。
    • 切片的声明方式为 []类型,如 []int 表示一个整数切片。
  • 初始化:
    • 数组的初始化可以使用数组字面值,通过在声明时直接赋值来初始化数组的元素。
    • 切片的初始化通常是通过 make 函数创建,也可以使用切片字面值进行初始化。
  • 传递方式:
    • 数组作为函数参数传递时是按值传递的,即在函数内部会复制整个数组。
    • 切片作为函数参数传递时是按引用传递的,即在函数内部操作的是原始切片的数据。
  • 长度属性:
    • 数组具有一个 len 函数,用于获取数组的长度。
    • 切片也有 len 函数,用于获取切片的长度,切片的长度可以动态变化。
  • 容量属性:
    • 数组没有容量的概念,其长度就是其容量。
    • 切片有一个 cap 函数,用于获取切片的容量,容量表示切片可增长的上限。
  • 空值:
    • 数组在声明时未初始化的元素会根据元素类型设置为零值。
    • 切片在声明时未初始化会被设置为 nil,表示一个空切片。
  1. map
func main() {
    // 使用 make 函数创建一个空的字符串到整数的映射(map)m。
    m := make(map[string]int)
    // 向 map m 中添加键值对,"one" 是键,1 是值。
    m["one"] = 1
    // 继续向 map m 中添加键值对,"two" 是键,2 是值。
    m["two"] = 2
    // 打印 map 的内容,输出:map[one:1 two:2]
    fmt.Println(m)
    // 使用 len 函数获取 map m 的元素个数,输出:2
    fmt.Println(len(m))
    // 使用 m["one"] 获取键为 "one" 的值,输出:1
    fmt.Println(m["one"])
    // 当获取不存在的键时,map 返回值类型的零值。
    fmt.Println(m["unknow"]) // 输出:0
    
    // 使用 m["unknow"] 获取键为 "unknow" 的值和是否存在的标志。
    // 因为 "unknow" 不存在于 map m 中,所以返回值 r 是 int 类型的零值 0,ok 是 false。
    r, ok := m["unknow"]
    fmt.Println(r, ok) // 输出:0 false

    // 使用 delete 函数删除 map m 中的键为 "one" 的键值对。
    delete(m, "one")

    // 使用 map 字面值的方式创建一个非空的字符串到整数的映射 m2 和 m3。
    m2 := map[string]int{"one": 1, "two": 2}
    var m3 = map[string]int{"one": 1, "two": 2}
    // 打印 m2 和 m3 的内容,输出:map[one:1 two:2] map[one:1 two:2]
    fmt.Println(m2, m3)
}
  • map 是一种无序的键值对集合,也被称为哈希表或字典。map 中的每个元素都由一个唯一的键(key)和相应的值(value)组成。其中,键必须是唯一且不可重复的,值可以重复。
  • 使用 len(map) 可以获取 map 的元素个数。
  • 通过 map[key] 可以获取指定键对应的值,如果键不存在,会返回值类型的零值。
  • 使用 delete(map, key) 可以删除 map 中的指定键值对。
  1. range
func main() {
    nums := []int{2, 3, 4}
    sum := 0
    // 使用 range 遍历整数切片 nums,i 是索引,num 是值。
    // 遍历过程中将每个元素的值累加到 sum 中,并打印出值为 2 的元素的索引和值。
    for i, num := range nums {
        sum += num
        if num == 2 {
            fmt.Println("index:", i, "num:", num) // 输出:index: 0 num: 2
        }
    }
    // 打印整数变量 sum 的值,输出:9
    fmt.Println(sum)


    m := map[string]string{"a": "A", "b": "B"}
    // 使用 range 遍历映射 m,k 是键,v 是值。
    // 遍历过程中打印每个键值对的键和值。
    for k, v := range m {
        fmt.Println(k, v) // 输出:b B; a A
    }
    // 使用 range 遍历映射 m,只遍历键。
    // 遍历过程中打印每个键。
    for k := range m {
        fmt.Println("key", k) // 输出:key a; key b
    }
}
  • range 关键字用于遍历数组、切片、字符串、map等数据结构。
  • 在遍历数组和切片时,range 返回元素的索引和值。
  • 在遍历字符串时,range 返回字符的索引和字符本身。
  • 在遍历映射(map)时,range 返回键和值。
  • 对于不需要索引或值的情况,可以使用下划线 _ 来忽略其中一个值。例如 for _, v := range nums,表示只获取切片元素的值而不获取索引。
  • range 遍历是一个安全的操作,它不会出现索引越界的情况。即使在遍历过程中修改了切片或映射,遍历会继续按照原来的长度和顺序进行。
  1. 函数
// 定义一个名为 add 的函数,接收两个整数参数 a 和 b,返回它们的和。
func add(a int, b int) int {
    return a + b
}

// 省略类型的函数定义,当参数类型相同时,可以将参数的类型省略。
func add2(a, b int) int {
    return a + b
}

// 定义一个名为 exists 的函数,接收一个字符串到字符串的映射 m 和键 k,返回键 k 对应的值和是否存在的标志。
func exists(m map[string]string, k string) (v string, ok bool) {
    v, ok = m[k]
    return v, ok
}

func main() {
    // 调用 add 函数,并将返回值赋给 res。
    res := add(1, 2)
    // 打印 res,输出:3
    fmt.Println(res)

    // 调用 exists 函数,并将返回值赋给 v 和 ok。
    v, ok := exists(map[string]string{"a": "A"}, "a")
    // 打印 v 和 ok,输出:A true
    fmt.Println(v, ok)
}
  • 在Go语言中,函数通过 func 关键字来定义。
  • 函数名后面跟着参数列表,参数列表中的每个参数都包含一个参数名和类型,多个参数之间使用逗号分隔。
  • 函数可以有一个或多个返回值,使用 return 关键字来返回结果。如果函数有多个返回值,可以使用括号将它们括起来。
  • 调用函数时,通过传入参数获取结果。函数的返回值可以被忽略,也可以通过赋值给变量来使用。
  • 可以使用 := 来定义和初始化函数的返回值,Go语言会根据返回值的类型自动推断。
  • 函数的参数和返回值可以是任意数据类型,包括基本类型、复合类型(切片、映射、结构体等)以及自定义类型。
  1. 指针
// 定义一个名为 add2 的函数,接收一个整数参数 n,但在函数内部对 n 进行修改并不会影响外部的 n。
func add2(n int) {
    n += 2
}

// 定义一个名为 add2ptr 的函数,接收一个整数指针参数 n,通过指针对原始变量 n 进行修改。
func add2ptr(n *int) {
    *n += 2
}

func main() {
    n := 5
    // 调用 add2 函数,并传入 n 作为参数。
    // 但因为传递的是 n 的值(拷贝),函数内部的修改不会影响外部的 n。
    add2(n)
    // 打印 n 的值,输出:5
    fmt.Println(n)

    // 调用 add2ptr 函数,并传入 n 的地址(指针)作为参数。
    // 由于传递的是指针,函数内部对原始变量 n 进行了修改。
    add2ptr(&n)
    // 打印 n 的值,输出:7
    fmt.Println(n)
}
  • Go语言中的指针是一种特殊的数据类型,用于存储其他变量的内存地址。
  • 通过指针,我们可以直接访问变量所在内存的值,而不是拷贝变量的值,从而可以在函数内部对变量进行修改,并影响到外部的变量。
  • 在函数调用时,参数传递可以是值传递或指针传递。值传递会将原始值的拷贝传递给函数,对函数内部的修改不会影响外部的变量。指针传递会将变量的内存地址传递给函数,对函数内部的修改会影响外部的变量。
  • 使用 * 运算符可以获取指针指向的值,使用 & 运算符可以获取变量的内存地址。
  • 在 Go 语言中,没有像 C++ 中的引用概念。Go 语言中的变量赋值和函数参数传递都是按值传递的,而不像 C++ 中的引用可以实现按引用传递。在 Go 语言中,如果想要在函数内部修改外部变量的值,需要使用指针来实现,通过传递变量的指针,可以在函数内部对原始变量进行修改,从而达到类似引用的效果。
  1. 结构体和结构体方法
// 定义一个名为 user 的结构体,包含两个字段 name 和 password。
type user struct {
    name     string
    password string
}

// 定义一个函数 checkPassword,用于检查结构体变量 u 的密码是否匹配。
func checkPassword(u user, password string) bool {
    return u.password == password
}

// 定义一个函数 checkPassword2,用于通过结构体指针检查结构体变量 u 的密码是否匹配。
func checkPassword2(u *user, password string) bool {
    return u.password == password
}

// 定义一个名为 checkPassword 的结构体方法,接收一个名为 password 的字符串参数。
// 这个方法的接收者是一个值类型的 user(非指针),因此该方法在调用时接收的是 user 的副本。
// 方法用于检查接收者 user 的密码是否匹配传入的 password,并返回一个布尔值。
func (u user) checkPassword(password string) bool {
    return u.password == password
}

// 定义一个名为 resetPassword 的结构体方法,接收一个名为 password 的字符串参数。
// 这个方法的接收者是一个指针类型的 *user,因此该方法在调用时接收的是 user 的指针。
// 方法用于重置接收者 user 的密码为传入的 password。
func (u *user) resetPassword(password string) {
    u.password = password
}

func main() {
    // 使用结构体字面值的方式创建结构体变量 a,字段值可以通过字段名初始化。
    a := user{name: "wang", password: "1024"}

    // 使用结构体字面值的方式创建结构体变量 b,可以直接赋值字段值,字段的顺序要和结构体定义一致。
    b := user{"wang", "1024"}

    // 使用结构体字面值的方式创建结构体变量 c,并只初始化其中一个字段值,其他字段值将使用默认零值(空字符串)。
    c := user{name: "wang"}
    c.password = "1024" // 对 c 结构体的字段进行单独赋值。

    // 声明结构体变量 d,并逐个字段赋值。
    var d user
    d.name = "wang"
    d.password = "1024"

    // 打印结构体变量 a、b、c、d 的值,输出:{wang 1024} {wang 1024} {wang 1024} {wang 1024}
    fmt.Println(a, b, c, d)

    // 调用函数 checkPassword 检查结构体变量 a 的密码,输出:false
    fmt.Println(checkPassword(a, "haha"))

    // 调用函数 checkPassword2 通过结构体指针检查结构体变量 a 的密码,输出:false
    fmt.Println(checkPassword2(&a, "haha"))

    // 调用结构体方法 resetPassword 来重置结构体变量 a 的密码。
    // 因为 resetPassword 方法的接收者是 *user(指针类型),所以在调用时使用 &a 获取指向结构体 a 的指针。
    a.resetPassword("2048")

    // 调用结构体方法 checkPassword 来检查结构体变量 a 的密码是否匹配传入的密码 "2048"。
    // 因为 checkPassword 方法的接收者是 user(值类型),所以在调用时使用结构体变量 a 的副本。
    // 输出:true
    fmt.Println(a.checkPassword("2048"))
}
  • Go 语言中的结构体是一种复合数据类型,用于将多个不同类型的数据组合在一起。
  • 可以通过结构体字面值的方式创建结构体变量,可以按字段名初始化,也可以按字段顺序赋值。
  • 结构体的字段可以通过点号(.)访问,使用 结构体变量.字段名 的形式来获取或修改字段的值。
  • Go 语言中的结构体方法是指与特定类型关联的函数。结构体方法允许为结构体类型定义属于自己的行为,这些方法可以在结构体变量上调用。方法的定义与函数类似,但在其前面加上了一个接收者,这个接收者指定了方法作用的目标类型。
  1. error
package main

import (
	"errors"
	"fmt"
)

type user struct {
	name     string
	password string
}

// 定义一个名为 findUser 的函数,接收一个 user 切片和一个字符串 name 作为参数。
// 函数会在 user 切片中查找与 name 匹配的用户,如果找到,则返回该用户的指针和 nil,表示无错误;
// 如果未找到,则返回 nil 和一个新的错误,表示找不到用户。
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() {
    // 调用 findUser 函数来查找名为 "wang" 的用户。
    // 因为 user 切片中包含名为 "wang" 的用户,所以函数会返回该用户的指针和 nil,表示无错误。
    u, err := findUser([]user{{"wang", "1024"}}, "wang")
    if err != nil {
        // 如果发生错误,打印错误信息并返回。
        fmt.Println(err)
        return
    }
    // 打印找到的用户的名字,输出:wang
    fmt.Println(u.name)

    // 使用 if 语句中的短变量声明来调用 findUser 函数,并查找名为 "li" 的用户。
    // 因为 user 切片中不包含名为 "li" 的用户,所以函数会返回 nil 和一个新的错误,表示找不到用户。
    // 在 if 语句中使用短变量声明声明了新的变量 u 和 err,只在 if 语句块中生效。
    if u, err := findUser([]user{{"wang", "1024"}}, "li"); err != nil {
        // 如果发生错误,打印错误信息并返回。
        fmt.Println(err) // 输出:not found
        return
    } else {
        // 打印找到的用户的名字,但在此代码块中的 u 和 err 变量是 if 语句块中的局部变量,不影响外部的 u 和 err。
        fmt.Println(u.name)
    }
}
  • 在 Go 语言中,error 是一个内置的接口类型,用于表示可能发生的错误。
  • 当函数遇到错误情况时,可以通过返回 error 类型来表示错误,并在调用函数的地方进行错误处理。
  • 可以使用 errors.New("error message") 创建一个新的错误,表示发生了指定的错误情况。
  • 在某些情况下,可以在 if 语句中使用短变量声明来检查函数返回的 error,并执行相应的错误处理逻辑。这样声明的变量只在 if 语句块内部有效,不会影响外部的同名变量。
  1. string方法
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 (中文字符使用 UTF-8 编码,一个中文字符占用 3 个字节)
}
  • Go 语言的字符串类型是不可变的,这意味着一旦创建了字符串,就不能修改它的内容。Go 提供了一组字符串方法,这些方法用于对字符串进行操作和处理。
  • strings.Contains(str, substr string) bool:检查字符串 str 中是否包含子字符串 substr,返回一个布尔值表示是否包含。
  • strings.Count(str, substr string) int:计算字符串 str 中子字符串 substr 出现的次数,并返回一个整数。
  • strings.HasPrefix(str, prefix string) bool:检查字符串 str 是否以 prefix 开头,返回一个布尔值。
  • strings.HasSuffix(str, suffix string) bool:检查字符串 str 是否以 suffix 结尾,返回一个布尔值。
  • strings.Index(str, substr string) int:返回字符串 str 中子字符串 substr 的第一个匹配的索引,如果没有找到,返回 -1
  • strings.Join(str []string, sep string) string:使用分隔符 sep 将字符串切片 str 中的所有元素连接起来,并返回一个新的字符串。
  • strings.Repeat(str string, count int) string:将字符串 str 重复 count 次,并返回一个新的重复后的字符串。
  • strings.Replace(str, old, new string, n int) string:将字符串 str 中的 old 子字符串替换为 new 子字符串,替换 n 次(如果 n 小于 0,则替换所有匹配项),并返回一个新的字符串。
  • strings.Split(str, sep string) []string:使用分隔符 sep 将字符串 str 分割成多个子字符串,并返回一个包含子字符串的切片。
  • strings.ToLower(str string) string:将字符串 str 中的所有字符转换为小写,并返回一个新的字符串。
  • strings.ToUpper(str string) string:将字符串 str 中的所有字符转换为大写,并返回一个新的字符串。
  • len(str string) int:返回字符串 str 的字节长度。
  1. fmt
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(s, n)
    // 打印结构体 p,输出:{1 2}
    fmt.Println(p)
    
    // 使用 fmt 包的 Printf 函数
    // 使用 %v 格式化输出,输出:s=hello
    fmt.Printf("s=%v\n", s)
    // 使用 %v 格式化输出,输出:n=123
    fmt.Printf("n=%v\n", n)
    // 使用 %v 格式化输出,输出:p={1 2}
    fmt.Printf("p=%v\n", p)
    // 使用 %+v 格式化输出,输出:p={x:1 y:2}
    fmt.Printf("p=%+v\n", p)
    // 使用 %#v 格式化输出,输出:p=main.point{x:1, y:2}
    fmt.Printf("p=%#v\n", p)
    
    f := 3.141592653
    // 使用 fmt 包的 Println 函数,打印浮点数 f,输出:3.141592653
    fmt.Println(f)
    // 使用 fmt 包的 Printf 函数,使用 %.2f 格式化输出,输出:3.14
    fmt.Printf("%.2f\n", f)
}
  • Go 语言中的 fmt 包提供了格式化输入和输出的功能,用于将数据格式化输出到标准输出或字符串。
  • fmt.Printffmt.Printlnfmt 包中最常用的函数,Printf 用于格式化输出,Println 用于打印一行数据并换行。
  • 在格式化字符串中,可以使用 %v 来表示将变量的值格式化输出。%+v 格式化输出会将结构体字段名一并打印出来,%#v 则会打印变量的 Go 语法表示。
  • %.2f 表示格式化浮点数时,保留小数点后两位。
  • fmt 包还提供了其他的格式化输出方式,例如 %d 表示整数,%s 表示字符串,%t 表示布尔值等。根据不同的需求,可以选择合适的格式化方式来输出数据。
  1. json
import (
    "encoding/json"
    "fmt"
)

// 定义一个名为 userInfo 的结构体,包含三个字段 Name、Age 和 Hobby。
// 结构体字段的标签 `json:"age"` 表示在 JSON 编码和解码时,使用 "age" 作为字段名。
type userInfo struct {
    Name  string
    Age   int    `json:"age"`
    Hobby []string
}

func main() {
    a := userInfo{Name: "wang", Age: 18, Hobby: []string{"Golang", "TypeScript"}}

    // 使用 json.Marshal 函数将 userInfo 结构体编码为 JSON 格式的字节数组。
    buf, err := json.Marshal(a)
    if err != nil {
        panic(err)
    }
    fmt.Println(buf)         // [123 34 78 97 ... ]
    fmt.Println(string(buf)) // {"Name":"wang","age":18,"Hobby":["Golang","TypeScript"]}

    // 使用 json.MarshalIndent 函数将 userInfo 结构体编码为格式化后的 JSON 字符串。
    buf, err = json.MarshalIndent(a, "", "\t")
    if err != nil {
        panic(err)
    }
    fmt.Println(string(buf))
    // 输出为:
    // {
    //     "Name": "wang",
    //     "age": 18,
    //     "Hobby": [
    //         "Golang",
    //         "TypeScript"
    //     ]
    // }

    var b userInfo
    // 使用 json.Unmarshal 函数将 JSON 格式的字节数组解码为 userInfo 结构体。
    // 注意:需要传递 userInfo 结构体的指针作为参数,以便函数能够修改结构体的值。
    err = json.Unmarshal(buf, &b)
    if err != nil {
        panic(err)
    }
    fmt.Printf("%#v\n", b) // main.userInfo{Name:"wang", Age:18, Hobby:[]string{"Golang", "TypeScript"}}
}
  • Go 语言中的 encoding/json 包提供了 JSON 数据与 Go 语言数据结构之间的编码和解码功能。
  • json.Marshal 函数用于将 Go 语言数据结构编码为 JSON 格式的字节数组,json.MarshalIndent 函数用于编码为格式化后的 JSON 字符串。
  • json.Unmarshal 函数用于将 JSON 格式的字节数组解码为指定的 Go 语言数据结构,并需要传递结构体的指针作为参数,以便函数能够修改结构体的值。
  • 可以通过结构体字段的标签 json:"field" 来指定在编码和解码时使用不同的字段名,这样可以灵活地处理字段名与 JSON 数据的对应关系。
  1. time
import (
    "fmt"
    "time"
)

func main() {
    // 获取当前时间:
    now := time.Now()
    fmt.Println(now) // 2022-03-27 18:04:59.433297 +0800 CST m=+0.000087933

    // 使用 time.Date 函数创建指定时间 t 和 t2。
    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) // 2022-03-27 01:25:36 +0000 UTC

    // 使用 time 对象的 Year、Month、Day、Hour、Minute 方法获取年、月、日、时、分等时间组成部分。
    fmt.Println(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute()) // 2022 March 27 1 25

    // 使用 time 对象的 Format 方法按指定的格式进行时间格式化。
    fmt.Println(t.Format("2006-01-02 15:04:05")) // 2022-03-27 01:25:36

    // 使用 time 对象的 Sub 方法计算两个时间之间的时间间隔 diff。
    diff := t2.Sub(t)
    fmt.Println(diff) // 1h5m0s
    // 使用 time.Duration 类型的方法获取时间间隔的分钟数和秒数。
    fmt.Println(diff.Minutes(), diff.Seconds()) // 65 3900

    // 使用 time.Parse 函数将字符串时间 "2022-03-27 01:25:36" 解析为 time 对象 t3。
    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) // true

    now := time.Now()
    // 使用 Unix 方法获取当前时间的 Unix 时间戳。
    fmt.Println(now.Unix()) // 1648738080
}
  • Go 语言的 time 包提供了处理时间和日期的功能。
  • 可以使用 time.Now() 获取当前时间。
  • 可以使用 time.Date() 创建指定的时间。
  • 可以使用 time.Format() 方法按照指定的格式对时间进行格式化输出。
  • 可以使用 time.Sub() 方法计算两个时间之间的时间间隔。
  • 可以使用 time.Parse() 方法将字符串时间解析为 time.Time 对象。
  • 可以使用 time.Unix() 方法获取时间的 Unix 时间戳。
  1. strconv
import (
    "fmt"
    "strconv"
)

func main() {
    // 使用 strconv.ParseFloat 函数将字符串 "1.234" 转换为 float64 类型的浮点数。
    f, _ := strconv.ParseFloat("1.234", 64)
    fmt.Println(f) // 1.234

    // 使用 strconv.ParseInt 函数将字符串 "111" 转换为 int64 类型的整数。
    n, _ := strconv.ParseInt("111", 10, 64)
    fmt.Println(n) // 111

    // 使用 strconv.ParseInt 函数将字符串 "0x1000" 转换为 int64 类型的整数。
    // 参数 base 为 0,表示根据字符串前缀判断数字的进制(0x 表示十六进制)。
    n, _ = strconv.ParseInt("0x1000", 0, 64)
    fmt.Println(n) // 4096

    // 使用 strconv.Atoi 函数将字符串 "123" 转换为 int 类型的整数。
    n2, _ := strconv.Atoi("123")
    fmt.Println(n2) // 123

    // 使用 strconv.Atoi 函数将字符串 "AAA" 转换为 int 类型的整数。
    // 如果转换失败,Atoi 函数会返回 0 和一个错误信息。
    n2, err := strconv.Atoi("AAA")
    fmt.Println(n2, err) // 0 strconv.Atoi: parsing "AAA": invalid syntax
}
  • Go 语言的 strconv 包提供了字符串和基本数据类型之间的相互转换功能。
  • 可以使用 strconv.ParseFloat() 将字符串转换为浮点数。
  • 可以使用 strconv.ParseInt() 将字符串转换为整数,base 参数指定进制。
  • 可以使用 strconv.Atoi() 将字符串转换为整数,如果转换失败,会返回 0 和一个错误信息。
  1. os
import (
    "fmt"
    "os"
    "os/exec"
)

func main() {
    // 使用 os.Args 可以获取命令行参数,第一个参数是程序的名称。
    // go run example/20-env/main.go a b c d
    fmt.Println(os.Args) // [C:\Users\Administrator\AppData\Local\Temp\go-build1619588333\b001\exe\main.exe a b c d]

    // 使用 os.Getenv 函数可以获取指定的环境变量的值。
    fmt.Println(os.Getenv("PATH")) // D:\go\bin;C:\Program Files (x86)\Common Files\MVS\Runtime\Win32_i86;C:\Program Files (x86)\Common Files\MVS\Runtime\Win64_x64;C:\Program Files (x86)\Common Files\Oracle\Java\javapath;D:\HuarayTech\MV Viewer\Runtime\x64\;D:\HuarayTech\MV Viewer\Runtime\Win32\;D:\VMware\bin\;D:\NVIDIA\bin;D:\NVIDIA\libnvvp;C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\Windows\System32\OpenSSH\;C:\Program Files\NVIDIA Corporation\Nsight Compute 2022.2.0\;D:\Qt5.12\5.12.9\msvc2017_64\bin;C:\Program Files (x86)\NVIDIA Corporation\PhysX\Common;C:\Program Files\NVIDIA Corporation\NVIDIA NvDLISR;D:\mingw64\bin;D:\Git\cmd;D:\mingw64\bin;D:\Python36\;D:\Python36\Scripts\;D:\jdk1.8.0_181;D:\android_SDK\sdk\platform-tools;D:\android_SDK\sdk\tools;D:\MATLAB2016a\runtime\win64;D:\MATLAB2016a\bin;D:\MATLAB2016a\polyspace\bin;D:\go\bin;C:\Users\Administrator\AppData\Local\Microsoft\WindowsApps;D:\Microsoft VS Code\bin;D:\opencv\build\x64\vc15\bin;D:\cmake-3.24.0-rc4-windows-x86_64\bin;D:\mingw64\bin;D:\Python36\;D:\Python36\Scripts\;

    // 使用 os.Setenv 函数可以设置环境变量的值。
    fmt.Println(os.Setenv("AA", "BB")) // 设置环境变量 AA 的值为 BB。

    // Linux
    // 使用 exec.Command 函数可以创建一个新的 cmd 对象,用于执行外部命令。
    cmd := exec.Command("grep", "127.0.0.1", "/etc/hosts")
    
    // 使用 CombinedOutput 方法可以执行外部命令并返回输出的结果和错误信息。
    buf, err := cmd.CombinedOutput()
    if err != nil {
        panic(err)
    }
    fmt.Println(string(buf)) // 输出 grep 命令在 /etc/hosts 文件中查找到的结果。
    
    // Windows
    // 在 Windows 下,使用 exec.Command 创建 cmd 对象,并执行 `dir` 命令来列出当前目录中的文件和文件夹。`/C` 参数告诉 cmd 执行完命令后关闭命令行窗口。而使用 `cmd /K` 可以执行一个命令并保持命令行窗口打开。
    cmd := exec.Command("cmd", "/C", "dir")

    // 使用 cmd.CombinedOutput 方法执行命令并获取输出结果和错误信息。
    output, err := cmd.CombinedOutput()
    if err != nil {
            fmt.Println("Error:", err)
            return
    }
    // 输出命令执行结果。
    fmt.Println(string(output))
}
  • Go 语言的 os 包提供了访问操作系统功能的接口。
  • 可以使用 os.Args 获取命令行参数。
  • 可以使用 os.Getenv 获取指定的环境变量的值,使用 os.Setenv 设置环境变量的值。
  • 可以使用 exec.Command 创建一个新的 cmd 对象,用于执行外部命令,然后使用 CombinedOutput 方法执行外部命令并返回输出结果和错误信息。
  1. 猜字游戏
package main

import (
    "bufio" // 提供了用于带缓冲的读写操作的功能。它可以在读取和写入数据时提供更高效的性能,特别是对于大量的小数据块,因为它会对输入和输出进行缓冲,减少了系统调用的次数,从而提高了 I/O 操作的效率。
    "fmt"
    "math/rand" // 提供了伪随机数生成器(pseudo-random number generator),可以生成不同类型的随机数,包括整数、浮点数和字节序列。
    "os"
    "strconv"
    "strings"
    "time"
)

func main() {
    // 设置最大随机数和随机数种子
    maxNum := 100
    // 这里使用 `rand.Seed(time.Now().UnixNano())` 来设置随机数生成器的种子,确保每次运行程序都会生成不同的随机数。
    rand.Seed(time.Now().UnixNano())
    secretNumber := rand.Intn(maxNum)

    fmt.Println("Please input your guess") // 提示用户输入猜测的数字
    reader := bufio.NewReader(os.Stdin) // 创建标准输入的 reader 对象
    for {
        input, err := reader.ReadString('\n') // 读取用户输入的字符串
        if err != nil {
            fmt.Println("An error occurred while reading input. Please try again", err)
            continue
        }

        input = strings.Trim(input, "\r\n") // 去除输入字符串中的换行符
        guess, err := strconv.Atoi(input) // 将输入的字符串转换为整数
        if err != nil {
                fmt.Println("Invalid input. Please enter an integer value")
                continue
        }

        fmt.Println("Your guess is", guess) // 打印用户猜测的数字
        // 根据用户猜测和秘密数字之间的大小关系,给出相应的提示
        if guess > secretNumber {
            fmt.Println("Your guess is bigger than the secret number. Please try again")
        } else if guess < secretNumber {
            fmt.Println("Your guess is smaller than the secret number. Please try again")
        } else {
            fmt.Println("Correct, you Legend!")
            break // 猜对了,退出游戏循环
        }
    }
}

rand:

  • 生成随机整数: 可以使用 rand.Int()rand.Intn(n)rand.Int31() 等函数生成随机整数。rand.Int() 会返回一个随机的大整数,rand.Intn(n) 会返回一个介于 0 和 n-1 之间的随机整数,rand.Int31() 会返回一个 31 位的非负随机整数。
  • 生成随机浮点数: 可以使用 rand.Float32()rand.Float64() 函数生成随机浮点数。rand.Float32() 返回一个介于 0.0 和 1.0 之间的随机 32 位浮点数,rand.Float64() 返回一个介于 0.0 和 1.0 之间的随机 64 位浮点数。
  • 生成随机字节序列: 可以使用 rand.Read() 函数生成随机的字节序列。rand.Read() 接受一个字节切片作为参数,并将随机的字节序列填充到该切片中。
  • 设置随机数种子: 可以使用 rand.Seed() 函数设置随机数种子。如果不手动设置种子,rand 包会默认使用时间种子(time.Now().UnixNano())作为种子,以确保每次运行程序都会生成不同的随机数序列。

bufio

  • 缓冲读取(Buffered Reading): bufio 可以将 os.Stdin(标准输入)或者其他 io.Reader(例如文件)包装成带缓冲的读取器,从而优化读取数据的性能。它提供了 bufio.NewReader() 函数用于创建带缓冲的读取器,可以使用 ReadString()ReadBytes()ReadLine() 等方法从输入源中读取数据,并将数据存储在内部缓冲区中,减少了实际的 I/O 操作次数。
  • 缓冲写入(Buffered Writing): bufio 可以将 os.Stdout(标准输出)或者其他 io.Writer(例如文件)包装成带缓冲的写入器,从而优化写入数据的性能。它提供了 bufio.NewWriter() 函数用于创建带缓冲的写入器,可以使用 Write()WriteString() 等方法将数据写入输出源,并将数据暂存在内部缓冲区中,减少了实际的 I/O 操作次数。
  • 行扫描(Line Scanning): bufio 提供了 Scanner 类型,可以通过 bufio.NewScanner() 创建行扫描器。行扫描器可以方便地从输入源中逐行读取数据,并且支持自定义的分隔符,如换行符。
  • 其他功能: bufio 还提供了其他一些功能,如 Peek() 方法可以在读取数据的同时预览缓冲区中的下一部分数据,Buffered() 方法可以获取缓冲区中的未读取数据长度等。