Go语言基础入门 | 青训营笔记

165 阅读14分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第1篇笔记

1. 简介

Go语言是Google出品的一门通用的计算机编程语言

2. 特点

  1. 高性能、高并发

    • 有和C++、Java媲美的性能

    • 内置对高并发的支持,其他编程语言以库的形式支持。在GoLang中不需要像其他语言一样去寻找经过高度性能优化的第三方库来开发应用,只需要使用标准库,或基于标准库的第三方库即可开发高并发应用程序

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

    • Go语言语法风格类似于C语言,并且在C语言的基础上进行了大幅度的简化:

      1. 吃掉了不必要的表达式括号

      2. 只有for循环一种表示方法,就可以实现数值、键值等遍历

    • 实现简单的Http服务器

      package main
      ​
      import {
          ”net/http“
      }
      ​
      func main() {
          http.Handle("")
      }
      
  3. 丰富的标准库

    • Go语言和Python一样,拥有及其丰富,功能完善,质量可靠的标准库。很多情况下,不需要借助第三方库就可以完成大部分基础功能的开发。大大降低了学习和使用成本

    • 最关键的是,标准库有很高的稳定性和兼容性保障,还能持续享受语言迭代所带来的性能优化,这时第三方库所不具备的。

  4. 完善的工具链

    • Go语言在诞生指出,就有着丰富的工具链

    • 不论是编译,代码格式化,错误检查,帮助文档,包管理还有代码补充提示,这些都有对应的工具

    • Go语言还完整内置了单元测试框架,能够支持单元测试,性能测试,代码覆盖率,数值竞争检测,这是保证代码正缺稳定运行的必备利器

  5. 静态链接

    • 在Go语言内所有的编译结果默认都是静态链接的,只需要拷贝唯一一个可执行的文件,不需要添加任何东西就可以部署运行。在线上容器环境运行,镜像体积可以控制的非常小,部署非常方便快捷。像C++需要附加一堆.source,才可以正确运行,如果不正确的话就会崩溃;java需要附加一个庞大的JRE才可以运行
  6. 快速编译

    • Go语言有静态语言里几乎最快的编译速度。在字节跳动,大量的微服务,在线上部署之前的编译小于1min。在真正本地开发的时候,几乎任何时候修改完一套代码都能够在1s种左右,增量编译完成。这个速度对于C++开发者来说几乎不可想象
  7. 跨平台

    • Go语言可以在常见的Windows、Linux、MaxOS的操作系统中运行

    • 也能够用来开发安卓、IOS软件

    • Go可以在各种设备上运行,包括:路由器、树莓派

    • Go语言还有很方便的交叉编译特性,能够轻易的在笔记本上编译出二进制拷贝到路由器上运行,还不需要配置交叉编译环境

  8. 垃圾回收

    • Go是一门带垃圾回收的语言,与Java类似

    • 写代码的时候,不需要考虑内存的分配释放,可以专注于业务逻辑

3. 基础语法&标准库

  1. Hello World

    package main // 代表这个文件属于main包的一部分。main包是程序的入口包,也就是程序的入口文件import (
        "fmt" // 导入了标准库种的format包,这个包是用来往屏幕输入输出字符串、格式化字符串
    )
    // main函数
    func main() {
        fmt.Println("hello world Cug")
    }
    
    // go run  运行
    // go build 编译
    
  2. 变量(var)

    Go是一门强类型语言,每一个变量都有自己的变量类型

    • 常见的变量类型:

      • 字符串

        • Go语言中字符串是内置类型,可以直接通过“+”号拼接

        • 也可以用“=”号去比较两个字符串

        • Go中大部分运算符的使用和优先级都和C/C++类似

      • 整数

      • 浮点型

      • 布尔型

    • 变量声明

      • Go中,变量的声明有两种方式:

        1. var 变量名 = 值

          这种方式一般会自动推导变量的类型,如果有需要也可以显式的写出变量类型

          var b, c int = 1, 2

        2. 变量名 := 值

          var a = "initial"
          
          var b, c int = 1, 2
          
          var d = true
          
          var e float64
          
          f := float32(e)
          
          g := a + "foo"
          fmt.Println(a, b, c, d, e, f) // initial 1 2 true 0 0
          fmt.Println(g)                // initialapple
          
  3. 常量(const)

    Golang的常量没有确定的类型,会根据使用的上下文自动确定类型

    const s string = "constant"
    const h = 500000000
    const i = 3e20 / h
    fmt.Println(s, h, i, math.Sin(h), math.Sin(i))
    
  4. if-else

    与C/C++类似,不同点在于:

    • if后面没有括号,如果有括号的话,编译器会自动去掉括号
    • 后面必须加大括号,不能像C++一样把if语句写在同一行
    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")
    }
    
  5. for

    Golang中没有while循环、没有do-while循环,只有for循环

    • 最简单的,for后面什么都不写就是死循环,也可以使用经典的C循环

    • continue(继续循环)、break(跳出循环)同C++

    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
    }
    
  6. switch

    与C/C++类似,switch后面的变量名不需要括号

    • 在C++中switch的case不加break会往下执行所有的case分支,Golang中不需要添加,不会跑到其他的分支。

    • Golang中的switch功能强大,可以使用任意类型的变量。比如字符串、结构体、甚至可以用来取代任意的if-else语句(switch后不加变量,在case内写逻辑分支)

    t := time.Now()
    switch {
        case t.Hour() < 12:
        fmt.Println("It's before noon")
        default:
        fmt.Println("It's after noon")
    }
    
  7. 数组

    数组是具有编号,且长度固定的元素序列。

    • 创建

      var a [5]int:可以存放5个int元素的数组a

      var twoD [2][3]int:可以存放2行3列的int元素的数组twoD

    真实环境中很少使用数组,基本上都是使用切片

  8. 切片(slice)

    切片不同于数组,是一种可变长度的数组,可以任意时刻去更改长度,也有更多丰富的操作。

    • 操作

      • 创建

        1. 可以通过make来创建一个切片:s := make([]string,3)

        2. 使用make创建slice的时候可以直接指定长度,这样可以避免频繁的扩容操作 s := make([]string, 3)

      • 插入

        1. 根据下标添加元素

          s[0] = "a"
          s[1] = "b"
          s[2] = "c"
          fmt.Println("get:", s[2])   // c
          fmt.Println("len:", len(s)) // 3
          
        2. 通过append追加元素

          (必须把append的结果赋值给原数组),因为Golang中slice原理是:存储了长度+ 容量+ 指向数组的指针。在执行append操作的时候,如果容量不够,会返回一个新的slice,所以需要赋值回去

          s = append(s, "d")
          s = append(s, "e", "f")
          fmt.Println(s) // [a b c d e f]
          
      • 拷贝

        • copy可以在两个slice之间拷贝数据

          c := make([]string, len(s))
          copy(c, s)
          fmt.Println(c) // [a b c d e f]
          
      • 切片

        • slice还拥有像python一样的切片操作:

          fmt.Println(s[2:5]) // [c d e](不包括第5个位置的元素)
          fmt.Println(s[:5])  // [a b c d e](不同于python,不支持负数索引)
          fmt.Println(s[2:])  // [c d e f]
          
  9. map

    其他编程语言中可能叫做哈希或者字典,是实际过程中使用最频繁的数据结构

    Golang中的map是完全无序的,遍历不会按照字母顺序或插入顺序输出,而是一个偏随机的顺序

    • 操作

      • 创建

        • 可以通过make创建一个空map

          m := make(map[string]int) //make(map[key的类型],value的类型)
          
      • 插入

        • 通过方括号语法去写入k-v对,然后也能通过方括号去读取k-v对

          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
          
      • 删除

        • 通过delete删除k-v对

          delete(m, "one")
          
      • 查询

        • 在读取k-v对的时候可以在接收参数后面加一个ok,通过ok判断map中,这个key是否存在

          r, ok := m["unknow"]
          fmt.Println(r, ok) // 0 false
          
  10. range

    对于slice和map可以通过range快速遍历,这样代码能够更加简洁

    • 对于数组,会返回两个值:[索引,值]。如果不需要索引的话,可以通过下划线“_”空位

      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
      
  • 对于map,会返回两个值:[key,value]

    m := map[string]string{"a": "A", "b": "B"}
    for k, v := range m {
       fmt.Println(k, v) // b 8; a A
    }
    for k := range m {
       fmt.Println("key", k) // key a; key b
    }
    
  1. 函数

    Golang的变量类型是后置的

    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
      }
      
  2. 指针

    Golang中指针的支持的操作非常有限,主要用途就是对传入的参数进行修改

    func add2(n int) {
    n += 2
    }
    
    • 这个n只在函数内被+2,函数外是不起作用的

      func add2ptr(n *int) { // n声明为指针类型
         *n += 2
      }
      
    • 为了类型匹配,调用时需要使用"&"符号,这样才能编译通过:add2ptr(&n);同时运算时在变量名前面加一个“*”号

  3. 结构体

    结构体是带字段的类型的集合

    type user struct {
       name     string
       password string
    }
    

    user结构体中包含了两个字段:name和password。

    • 我们可以通过结构体的名称去初始化一个结构体变量,初始化的时候可以传入每一个字段的初始值;也可以传入一部分字段的值,没有传值的字段初始化的值就是空值,字符串就是空字符串:

      a := user{name: "wang", password: "1024"}
      b := user{"wang", "1024"}
      c := user{name: "wang"}
      
    • 对于结构体,可以通过:结构体类型变量.字段名读取或者写入结构体字段的内容

      var d user
      d.name = "wang"
      d.password = "1024"
      
    • 结构体也能够作为函数的参数,作为参数有指针和非指针两种用法

      fmt.Println(checkPassword(a, "haha"))   // false
      fmt.Println(checkPassword2(&a, "haha")) // false
      

      如果是指针,能够实现对结构体的修改,以及在某些情况下避免大结构体拷贝的开销

  4. 结构体方法

    Golang中可以为结构体定义一些方法,这类似于其他语言中的类成员函数

    • 实现结构体方法有两种写法:(1)带指针;(2)不带指针

    • 带指针能够对结构体进行修改

      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
      }
      
  5. 错误处理

    Golang是通过返回一个单独的返回值来处理错误信息

    • 不同于Java语言当中的异常,Go语言的方式能够很清晰的知道哪个函数返回了错误,并且能够用简单的if-else去处理错误

    • 在函数中,我们可以在函数的返回值类型里加一个error(这就代表这个函数可能返回错误),当然return的时候就需要返回两个值

      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")
      }
      
      • 如果出现错误则返回:return nil, errors.New("not found")(这个error可以通过errors.New创建)
      • 如果没有错误则返回:return &u, nil
    • 调用这个返回错误的函数时,需要使用两个值来接收返回值。调用函数之后需要判断err是否存在,如果err存在需要做一些处理(打印、返回函数)。只有当err不存在时在能够取得真正的返回值,否则程序会出现空指针错误

      u, err := findUser([]user{{"wang", "1024"}}, "wang")
      if err != nil {
         fmt.Println(err)
         return
      }
      
  6. 字符串操作

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
  1. 字符串格式化
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

Println(打印换行)

  • %v:打印任何类型的变量,而不需要区分
  • %d:打印数字,%s打印字符串
  • %+v:打印字段的名称和值
  • %#v:打印结构体整个构造的类型名称以及字段的名称和值
  1. JSON处理

    Golang中,只需要保证结构体中的每个字段首字母大写(即,在Golang中的公开字段),那么这个结构体就可以用json.Marshal进行序列化,之后会变为一个byte数组,可以简单理解为字符串。

    a := userInfo{Name: "wang", Age: 18, Hobby: []string{"Golang", "TypeScript"}}
    buf, err := json.Marshal(a)
    if err != nil {
       panic(err)
    }
    
    • 打印的时候需要string()强制类型转换才能打印出字符串,否则会打印出一些16进制的编码:

      fmt.Println(buf)         // [123 34 78 97...]
      fmt.Println(string(buf)) // {"Name":"wang","age":18,"Hobby":["Golang","TypeScript"]}
      
    • 序列化之后的字符串也可以简单的用json.Unmarshal去反序列化到一个空的变量里面

      var b userInfo
      err = json.Unmarshal(buf, &b)
      
    • 正常情况下,序列化出的字符串它的风格是大写字母开头

      {"Name":"wang","age":18,"Hobby":["Golang","TypeScript"]}
      
    • 如果需要输出小写,下划线风格的话,可以在结构体字段后面加一个‘json age’的ta'g打印出来就是小写

      type userInfo struct {
         Name  string
         Age   int `json:"age"`
         Hobby []string
      }
      
      {"Name":"wang","age":18,"Hobby":["Golang","TypeScript"]}
      
  2. 时间处理

    • 最常见的时间处理就是:

      now := time.Now() // 获取当前时间
      

      或者通过:

      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
      
    • 还可以做两个时间的减法

      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
      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) // 时间段
      
    • 还可以格式化字符串为时间,不同于其他语言要求yyyy-mm-dd的格式,Golang官方定义了自己特定的格式

      fmt.Println(t.Format("2006-01-02 15:04:05")) 
      t3, err := time.Parse("2006-01-02 15:04:05", "2022-03-27 01:25:36")
      
    • 在和某些系统交互的时候会经常用到时间戳

      fmt.Println(now.Unix()) // 1648738080
      
  3. 数字解析

    字符串和数据之间的转换相关的操作都在strconv包下

    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") //也可以通过Itoa将数字转换为字符串
       fmt.Println(n2) // 123
    
       n2, err := strconv.Atoi("AAA")
       fmt.Println(n2, err) // 0 strconv.Atoi: parsing "AAA": invalid syntax
    }
    
    • 可以传入三个参数:字符串,进制(如果传入0表示自动推测进制),精度
  4. 进程信息

    Golang可以通过”os os/exec“获取进程在执行时的一些命令行参数

    • 例如执行go run example/20-env/main.go a b c d

      fmt.Println(os.Args)           
      // [/var/folders/8p/n34xxfnx38dg8bv_x8l62t_m0000gn/T/go-build3406981276/b001/exe/main a b c d]
      

      长度是5,第一个成员代表二进制自身的路径+名字;之后代表传入的参数

    • 此外,还可以获取或者写入环境变量

      fmt.Println(os.Getenv("PATH")) // /usr/local/go/bin...
      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
      

后记

本篇笔记是参加「第三届青训营 -后端场」课程后创作,着重讲了一下我对Go语言的一些理解。其中,关于语法与数据结构操作还有很多值得深入学习的地方。