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

150 阅读14分钟

实践:Go 语言入门指南:基础语法和常用特性解析

本文简要的探讨了 Go 语言的基础语法和常用特性。从变量声明和函数到数据类型、指针和错误处理等,涵盖了初学者需要掌握的关键概念。

Go特点

1. 高性能,高并发

Go 语言通过轻量级的 Goroutine 和基于消息传递的通信模型实现了高并发。Goroutine 是一种比线程更轻量级的并发单元,使得同时运行大量的并发任务成为可能。Go 的调度器(Scheduler)能够有效地在多个 Goroutine 之间分配 CPU 时间,从而提供了出色的并发性

2. 语法简单,学习曲线平缓

Go 语言的语法设计简洁明了,去掉了许多传统语言中的冗余和复杂性。它遵循 C 风格的语法,但精简了一些特性,使得初学者能够相对轻松地上手。Go 语言的标准库也是有意设计为简单且易于使用的,这有助于减少学习曲线。

3. 丰富的标准库

Go 语言提供了广泛且强大的标准库,涵盖了网络编程、文件操作、文本处理、加密、并发等方面。这使得开发者可以在不用依赖第三方库的情况下,快速构建各种类型的应用程序。

4. 完善的工具链

Go 语言自带了丰富的工具集,如编译器、格式化工具、测试工具、性能分析工具等。这些工具的存在有助于提高开发效率,使得开发者能够更专注于解决问题而不是处理繁琐的编译和调试过程。

5. 静态编译

Go 语言的编译器会将代码编译成机器码,这意味着应用程序不需要在运行时依赖外部的运行环境或解释器。这样可以确保应用程序在不同环境中的稳定性和一致性,同时也有助于提高执行效率。

6. 快速编译

Go 语言的编译速度非常快,这在开发过程中非常有益。快速的编译能够加快开发-测试-调试循环,使得开发者能够更迅速地验证代码变更的效果。

7. 跨平台

Go 语言的编译器支持多种操作系统和体系结构,这使得开发者可以轻松地在不同平台上编译和运行代码。一次编写,多平台运行的能力有助于简化跨平台应用程序的开发流程。

8. 垃圾回收

Go 语言具有自动的垃圾回收(Garbage Collection)机制,开发者无需显式地管理内存分配和释放。这有助于避免内存泄漏和悬挂指针等问题,提高了程序的稳定性和安全性。同时,Go 的垃圾回收算法也被优化为在高并发环境下工作良好,不会对程序性能造成过大的影响

Go基础

Ⅰ. 结构

```go
package main //包声明

import "fmt" //包导入

func main() { 
   /* 这是我的第一个简单的程序 */
   fmt.Println("Hello, World!")
}
```

运行: go run xxx.go 同时还可以使用 go build xxx.go命令来生成二进制文件

注意: { 不能单独放在一行,不然会报错 当标识符(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,如:Group1,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包),这被称为导出(像面向对象语言中的 public);标识符如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的(像面向对象语言中的 protected )。

Ⅱ. 语法

1. 变量声明

变量来源于数学,是计算机语言中能储存计算结果或能表示值抽象概念。

变量可以通过变量名访问。

Go 语言变量名由字母、数字、下划线组成,其中首个字符不能为数字。

声明变量的一般形式是使用 var 关键字:

        var a = "initial"
        var b, c int = 1, 2
        var d = true
        var e float64
        f := float32(e)
        g := a + "a"
        fmt.Println(a, b, c, d, e, f) //output: initial 1 2 true 0 0
        const s string = "constant"

局部变量:在函数体内声明的变量称之为局部变量,它们的作用域只在函数体内,参数和返回值变量也是局部变量。

全局变量:在函数体外声明的变量称之为全局变量,全局变量可以在整个包甚至外部包(被导出后)使用。

1.局部变量不会一直存在,在函数被调用时存在,函数调用结束后变量就会被销毁,即生命周期。

2.Go 语言程序中全局变量与局部变量名称可以相同,但是函数内的局部变量会被优先考虑。

2. 条件分支

switch 语句用于基于不同条件执行不同动作,每一个 case 分支都是唯一的,从上至下逐一测试,直到匹配为止。

switch 语句执行的过程从上至下,直到找到匹配项,匹配项后面也不需要再加 break。

switch 默认情况下 case 最后自带 break 语句,匹配成功后就不会执行其他 case,如果我们需要执行后面的 case,可以使用 fallthrough 。

        if 7%2 == 0; {
            // do something...
        } else {
            // do something...
        }

        switch a{ // 默认不存在switch穿透
            case 1:
            case 2:
                fallthrough
            case 3:
            default:
        }

        switch{
            case t.Hour() < 12:
                fmt.Println("xxx")
            default:
        }

3. 循环

一、

        for init; condition; post {
            // do something...
        }
  • 标准的 for 循环结构,通常用于迭代次数已知的情况。
  • init 部分在循环开始前执行一次,通常用于初始化变量。
  • condition 部分是一个布尔表达式,决定是否继续循环。
  • post 部分在每次循环结束后执行,通常用于更新循环控制变量。

二、

        for condition { 
            // 类似while
        }
  • 这是一个只有 condition 部分的 for 循环,类似于 while 循环。
  • 只要 condition 部分为真,循环将一直执行。

三、

        for{
            // while(true)
        }
  • 这是一个没有 initconditionpost 部分的无限循环。
  • 类似于 while (true),循环将无限执行,直到出现 break 或其他中断条件。

四、

        for key, value := range xxx{
            fmt.Println(key, value)
        }
  • 这是使用 range 关键字进行迭代的示例,适用于迭代集合(如数组、切片、映射等)的元素。
  • 循环会遍历集合中的每个元素,将索引和对应的值分别赋给 keyvalue 变量。
  • 这种循环方式适用于需要同时访问索引和值的情况,比如遍历数组或映射。

五、

        for _, value := range xxx{
            fmt.Println(value)
  • 这是使用 range 关键字进行迭代的示例,但在这里只关注值而忽略索引。
  • _ 通常用作占位符,表示在这里我们不关心索引。
  • 这种循环方式适用于仅需要迭代值的情况,比如遍历数组或切片中的元素。

4. 数组

数组是具有相同唯一类型的一组已编号且长度固定的数据项序列,这种类型可以是任意的原始类型例如整型、字符串或者自定义类型。

        var a [5]int
        a[4] = 100
        fmt.Println(len(a))

        b := [5]int{1, 2, 3, 4, 5}
        c := [...]int{1, 2, 3} // 长度自动推断

        var 2d [2][3]int

5. 切片 (slice) 切片类似一种动态数组,有容量属性与实际长度

Go 数组的长度不可改变,在特定场景中这样的集合就不太适用,Go 中提供了一种灵活,功能强悍的内置类型切片("动态数组"),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。

        make([]T, length, capacity)

        s := make([]string, 3)
        s = append(s, "a", "b", "c", "d", "f")
        c := make([]string, len(s))
        copy(c, s)
        good := []string{"g", "o", "o", "d"}
        fmt.Println(s[2:5]) // output: [c d e]
        fmt.Println(s[:5]) // output: [a b c d e]
        fmt.Println(s[2:]) // output: [c d e]

6. map

Map 是一种无序的键值对的集合。

Map 最重要的一点是通过 key 来快速检索数据,key 类似于索引,指向数据的值。

Map 是一种集合,所以我们可以像迭代数组和切片那样迭代它。不过,Map 是无序的,遍历 Map 时返回的键值对的顺序是不确定的。

在获取 Map 的值时,如果键不存在,返回该类型的零值,例如 int 类型的零值是 0,string 类型的零值是 ""。

Map 是引用类型,如果将一个 Map 传递给一个函数或赋值给另一个变量,它们都指向同一个底层数据结构,因此对 Map 的修改会影响到所有引用它的变量。

        m := make(map[string]int) // string: int
        m["one"] = 1
        r, ok := m["one"] // r = 1, ok = true
        delete(m, "one")
        m2 := map[string]int{"one": 1}
        var m3 = map[string]int{"one": 1}

7. 指针

Go中,指针是一种特殊的数据类型,用于存储变量的内存地址。通过指针,你可以直接访问和修改存储在内存中的变量的值。Go 语言中的指针具有以下特点:

  1. 声明指针:通过在变量名前加上 * 来声明指针类型。例如,var ptr *int 声明了一个指向整数的指针。
  2. 取地址操作符:使用 & 符号可以获取一个变量的内存地址。例如,x := 42ptr := &xptr 设置为变量 x 的地址。
  3. 解引用操作符:通过在指针变量前加上 * 来访问指针指向的值。例如,val := *ptr 会将指针 ptr 指向的值赋给变量 val
  4. 空指针:在声明指针时,如果没有显式初始化它,它会被自动赋值为 nil,表示一个空指针,即指向空地址。
        func addPtr(n *int){
            *n += 2
        }
        func main(){
            n := 5
            addPtr(&n)
            fmt.Println(&n != nil) // true
        }

8. 结构体

Go中的结构体(struct)是一种复合的数据类型,允许将不同类型的数据字段组合在一起,形成一个自定义的数据结构。结构体可以用于表示真实世界中的实体、对象或数据记录,从而更好地组织和管理数据。结构体的字段可以是不同类型的数据,如整数、字符串、其他结构体等。

结构体的特性和用途

  • 结构体允许将不同类型的数据字段组合在一起,用于表示复杂的数据结构。
  • 结构体可以作为函数的参数和返回值,用于传递和返回复杂的数据。
  • 结构体可以用于创建自定义的数据类型,以便更好地组织和管理代码。
  • 结构体方法可以附加在结构体上,实现面向对象的行为。
  • 结构体可以嵌套在其他结构体中,以构建复杂的数据层次结构。
        type user struct{
            name string
            password string
        }
        func (u user) checkPassword(password string) bool{ // user成员函数
            return password == u.password
        }
        func checkPassword(password string, pass string) bool{ // 非成员函数
            return password == pass
        }

9. 错误处理

Go提供了一种强大而简洁的错误处理机制,通过使用内置的 error 接口和返回错误值的约定,可以有效地管理和处理错误。

错误处理最佳实践

  • 返回错误值:函数应该返回明确的错误值,以便调用者可以检查错误。
  • 避免 panic:避免使用 panic 来表示错误,除非是不可恢复的情况。
  • 错误链:使用 fmt.Errorf 为错误添加上下文信息。
  • 错误类型:对于常见的错误情况,可以定义自己的错误类型,以便更好地识别错误。
  • 延迟函数:使用 defer 关键字确保资源清理和错误处理不被遗漏。
        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(){
            if u, err := findUser([]user{{"wang", 1024}}, "wang"); err != nil{
                fmt.Println(err)
            }else {
                fmt.Println(u.name)
            }
            
        }

10. 字符串操作

Go提供了多种字符串操作方法,用于处理、修改和操作字符串数据。这些方法包括字符串拼接、长度获取、字符访问、遍历、切片、比较、查找、替换、分割、大小写转换、修剪、格式化等。通过这些方法,开发者可以轻松地进行字符串的处理和转换,从而更方便地操作文本数据。

        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

11. 格式化输出

Go 中使用 fmt.Sprintf 或 fmt.Printf 格式化字符串并赋值给新串:

  • Sprintf 根据格式化参数生成格式化的字符串并返回该字符串。
  • Printf 根据格式化参数生成格式化的字符串并写入标准输出。
        type point struct{
            x, y int
        }
        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.1415
        fmt.Println(f) //3.1415
        fmt.Printf("%.2f\n", f) //3.14

12. JSON

Go 语言自带的 encode/json 包提供了对 JSON 数据格式的编码和解码能力。

只有选择用大写字母开头的字段名称,导出的结构体成员才会被编码。

在编码时,默认使用结构体字段的名字作为 JSON 对象中的 key,我们可以在结构体声明时,在结构体字段标签里可以自定义对应的 JSON key

        package main

        import (
            "encoding/json"
            "fmt"
        )
        type userInfo struct{
            Name string
            Age int `json:"age"` //自定义键名
            IdCard string `json:"-"` //忽略字段
        }
        func main(){
            a := userInfo{Name: "wang", Age: 18}
            buf, err := json.Marshal(a)
            if err != nil {
                panic(err)
            }
            fmt.Println(buf) //[123 34 78 ...] 十六进制数字
            fmt.Println(string(buf)) //{"Name":"wang", "age":18}
            
            buf, err = json.MarshalIndent(a, "", "\t") //整齐缩进的输出
            //该函数有两个额外的字符串参数用于表示每一行输出的前缀和每一个层级的缩进

            var b userInfo
            err = json.Unmarshal(buf, &b)
            if err != nil{
                panic(nil)
            }
            fmt.Println(\"%#v\n", b) //main.userInfo{Name:"wang", Age:18}
        }

13. 时间处理

Go 提供了强大的时间处理能力,使得处理时间和日期变得简单和可靠。Go 的时间包 time 提供了许多函数和方法,用于创建、解析、格式化和计算时间。这使得在应用程序中处理各种时间相关的任务变得更加容易。

时间的不可变性

Go 中的时间是不可变的,一旦创建就无法更改。任何修改操作都会返回一个新的时间实例。

注意事项

  • 在处理时间的过程中,务必考虑时区和夏令时等因素,以避免出现时间计算错误。
  • 在涉及到时间的比较和计算时,尽量使用 time.Time 类型,而不是直接使用时间戳。
  • 避免使用 time.Sleep() 阻塞主线程,可以使用定时器来执行定时任务。
        import (
            "fmt"
            "time"
        )
        func main(){
            now := time.Now() //2022-03-27 18;04:59.433297 +0800 CST m=+0.000087933
            t := time.Date(2022, 3, 27, 1, 25, 36, 0, time.UTC)
            fmt.Println(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute()) // 2022 March 27 1 25
            fmt.Println(t.Format("2006-01-02 15:04:05")) // 2022-03-27 01:25:36
            diff := t2.Sub(t) //时间差
            fmt.Println(diff) //1f5m0s
            fmt.Println(diff.Minutes, diff.Seconds()) //65 3900
            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
            fmt.Println(now.Unix()) //1648738080
        }

14. 数字解析

Go中的strconv包含了许多对数字进行解析转换的函数,如ParseFloat可以将字符串转为float,ParceInt将字符串转为int,当然,前提是 字符串中的内容就是数字,否则会报错,还有其他函数如下所示

注意事项

  • 在解析数字时,务必处理解析可能失败的情况,因为输入可能不符合预期的格式。
  • 使用适当的错误处理机制,如错误返回或 panic,来处理解析失败的情况。
  • 注意数字解析可能引发的类型溢出问题,确保使用足够大的数据类型来存储解析后的值。
        import(
            "fmt"
            "strconv"
        )

        func main(){
            f, _ := strconv.ParseFloat("1.234", 64) //f=1.234
            n, _ := strconv.ParseInt("111", 10, 64) //n=111
            n, _ := strconv.ParseInt("0x1000", 0, 64) //0表示自动推测 生成64位
            n2, _ := stcconv.Atoi("123") //n2=123
            n2, err := strconv.Atoi("AAA") //0 strconv.Atoi: parsing "AAA": invalid syntax
        }

15. 进程信息

Go中,要获取关于当前进程的信息,通常需要使用操作系统相关的包,如 os 包和 runtime 包。

        import (
            "fmt"
            "os"
            "os/exec"
        )

        func main(){
            //go run main.go a b c d
            fmt.Println(os.Args) //[C:\Users\xxx\AppData\Local\Temp\go-build3188608185\b001\exe\test.exe a b c d]

            fmt.Println(os.Getenv("PATH"))
            // fmt.Println(os.Setenv("AA", "BB"))
            buf, err := exec.Command("grep", "127.0.0.1", "/etc/hosts").CombinedOutput()
            if err != nil {
                panic(err)
            }
            fmt.Println(string(buf)) // 127.0.0.1 localhost
        }