👨💻本文中的源码地址:https://github.com/wangkechun/go-by-example 
学习目录
 
1. Go 简介 介绍下 Go 语言的特性以及目前 Go 语言在市场上的 “帝位”。
1.1 Go 语言特性 
高性能 & 高并发:Go 语言天生支持高并发(goroutine & channel & 调度器 ),不像很多语言通过库的形式支持;Go 像一些低级别的语言 (C/C++) 一样是一门编译型语言 ,这意味着它的性能足以媲美 C/C++! 
语法简洁易懂:语法类似 C 语言,比如:同时去掉了不需要的 (),循环也只简化成 for 循环这一种表示… 该特点使得用 Go 编写的代码易于维护。 
丰富的标准库:Go 语言带有极其丰富与完善的标准库,无需再借助第三方库完成便可以应对日常的开发 ,而且能持续享受到语言迭代带来的性能优化。 
完整的工具链:编译、代码格式化、错误检查、包管理、代码补全等都有相应的工具,Go 语言还内置了单元测试框架 (单元测试、性能测试、代码覆盖率、性能优化)。 
静态链接:所有的编译结果默认都是静态链接的,只需要拷贝编译后唯一的可执行文件即可部署运行 ,线上发布的体积可以控制得很小。 
快速编译:Go 语言一开始设计就考虑到快速编译。它能像其他解释型语言一样(Python & JavaScript),你不会注意它正在编译。 
跨平台:Go 语言能在 Linux、MacOS、Windows 等操作系统下运行,还能用于开发 Android、iOS 软件,甚至能运行在路由器、树莓派等等设备;同时具备交叉编译 特性。 
垃圾回收:Go 无需考虑内存的分配与释放,因为其内存由 Go 自身进行管理,不同于 C/C++,和 Java 类似。 
 
简洁的 Go 语法
 
1.2 拥抱 Go 语言 目前字节跳动已全面拥抱 Go 语言。除此之外,哔哩哔哩、七牛云、腾讯、百度、美团、Facebook、Google、Twitter、滴滴、深信服、知乎、去哪儿、360、微博等公司也在大量使用 Go 语言。
Go 语言在云计算、微服务、大数据、区块链、物联网等领域蓬勃发展,Docker、Kubernetes、etcd 等几乎所有的云原生组件全都是用 Go 语言实现。
2. Go 入门 😆这部分简单概括下如何搭建 Go 的开发环境 ,浏览下 Go 语言的基础语法 & 标准库 。
2.1 搭建开发环境 2.1.1 下载安装 Golang 
2.1.2 配置 Golang IDE ⭐首选目前市场上使用最广泛的两款 IDE : VSCode  & GoLand 
VSCode:由微软公司开发,免费。能运行在 MacOS、Windows、Linux 上的跨平台开源代码编辑器(功能齐全的 IDE),需要在扩展市场中安装 Go 插件才能支持 Golang 开发。 
GoLand:由 JetBrains 公司开发,付费 (学生可申请免费使用)。 
 
2.1.3 云上开发环境 我们还可以使用基于 GitHub 的 Gitpod  在线编程环境来使用 golang,只需在你的开源仓库 URL 的 https:// 替换成 https://gitpod.io/# 即可。
2.2 基础语法 & 常用标准库 这部分开始正式学习 Golang 的语法以及常用标准库的使用。
2.2.0 从 Hello World 见证 Go 程序的运行 1 2 3 4 5 6 7 8 9 package  mainimport  (    "fmt"  ) func  main ()   {    fmt.Println("hello world" ) } 
 
有 2 种方式运行该段程序:
go run 命令可直接运行程序 
go build 可将程序编译成二进制,编译完成后直接执行 ./main 即可运行 
 
2.2.1 变量 & 常量 Golang 是一门强类型语言(每个变量都有其对应的类型),变量 var 如果没有标注类型会自动推导,常量 const 默认没有确定类型(自动推导)。
注意:如果定义了变量,必须得使用,否则编译不通过
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 package  mainimport  (    "fmt"      "math"  ) var  x string  = "global"  const  (    A = iota         B               C = iota         D = "test"       E               F = 9            G           ) func  main ()   {              var  a = "掘了"                var  b = true                var  c, d int  = 1 , 9      var  e, f = 6 , "F"      var  (         g = 666          h = false      )               i := 1.9                j := float32 (i)               k := "掘金"  + a               y, _ := 1 , 2                c, d = d, c               const  l string  = "constant"      const  m = 10      const  n = 2e3  / m               fmt.Println(x, a, b, c, d, e, f, g, h, i, j, k, l, m, math.Abs(n), y)               fmt.Println(A, B, C, D, E, F, G) } 
 
2.2.2 数据类型 & 占位符 🌗Go 语言基本数据类型较多,主要有以下这些:
byte、int、int8、int16、int32、int64、uint… 
float32、float64 
error 
string 
bool 
rune 
 
🌓Go 语言囊括的复合数据类型范围较广 (下文慢慢介绍) :
数组 array 
切片 slice 
字典 map 
函数 func 
结构体 struct 
通道 channel 
接口 interface 
指针 * 
 
🚀根据数据特点又可分为值传递  & 引用传递 :
值传递:int、float、string、bool、array、struct 
引用传递:slice、map、chan、* 
 
⭐至于运算符 (算法运算符、关系运算符、逻辑运算符、位运算符、赋值运算符)和其他语言基本一致,这里不再赘叙。
2.2.3 if-else 条件判断 Go 语言中的 if 判断语句不需要 ();同理,switch 和 for 也不需要。
Go 语言中的 if 语句存在一种特殊的写法:
err 是 myFunc() 的返回值,执行后再对 err==nil 语句进行判断。
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package  mainimport  "fmt" func  main ()   {         if  5 %2  == 0  {         fmt.Println("5 is even" )     } else  {         fmt.Println("5 is odd" )     }               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" )     } } 
 
2.2.4 for 循环 Go 语言中没有 while、do while 这类循环,仅仅只有 for 这一种循环。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package  mainimport  "fmt" func  main ()   {         i := 1      for  i <= 3  {         fmt.Println(i)         i = i + 1      }          for  j := 7 ; j < 9 ; j++ {         fmt.Println(j)     }               for  {         fmt.Println("endless loop" )     } } 
 
2.2.5 switch 分支 相比 C/C++,Go 语言的 switch 分支略有不同 ,也更加强大 。
不同之处:case 中不需要再显式加 break,执行完对应 case 就会退出,不像 C/C++ 没加 break 会跑完余下所有 case;同时 switch 后也不再需要 ()。 
强大之处:Go 语言中 switch 后能跟任意变量类型;也可不跟任何变量,然后在 case 中写条件分支,这样就将 switch-case 分支结构简化为 if-else 条件判断结构了。 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 package  mainimport  (    "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" )     } } 
 
2.2.6 数组 array 数组是一个长度固定的元素序列,可利用索引取值/存值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 package  mainimport  "fmt" func  main ()   {         var  a [5 ]int      a[4 ] = 100      fmt.Println("get:" , a[2 ])        fmt.Println("len:" , len (a))                b := [3 ]int {1 , 2 , 3 }      fmt.Println(b)                      c := [...]int {1 , 3 , 2 }      fmt.Println(c)                        d := [...]int {1 : 3 , 6 : 5 }      fmt.Println(d)                           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)  } 
 
🚫不过在真实的业务场景中,我们很少直接使用定长的数组,更多使用的是切片。
2.2.7 切片 slice 数组是定长的,所以 Go 推出可扩容的切片。与数组不同的是,切片不需要指定 [] 里的长度。
1 2 3 4 5 6 7 8 9 10 var  slice1 []string var  slice2 = make ([]int , 3 )slice3 := []string {"g" , "o" , "o" , "d" } slice4 := make ([]int , 3 ) slice5 := make ([]int , 3 , 5 ) 
 
通常情况下,我们会使用 make 函数来创建一个切片;使用 append 来追加元素,注意要将其结果赋值给原切片;然后可以像数组一样去取值;还可以通过 [a:b] 来获取切片中指定范围的值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 package  mainimport  "fmt" func  main ()   {         s := make ([]string , 3 , 5 )     s[0 ] = "a"      s[1 ] = "b"      s[2 ] = "c"      fmt.Println("get:" , s[2 ])        fmt.Println("len:" , len (s))      fmt.Println("cap:" , cap (s))                s = append (s, "d" )     s = append (s, "e" , "f" )     fmt.Println(s)                c := make ([]string , len (s))     copy (c, s)     fmt.Println(c)           cs := []string {"w" , "w" , "w" , "w" , "w" }     copy (cs, s[:3 ])     fmt.Println(cs)                fmt.Println(s[2 :5 ])      fmt.Println(s[:5 ])       fmt.Println(s[2 :])                 good := []string {"g" , "o" , "o" , "d" }     fmt.Println(good)                good = append (good, c...)     fmt.Println(good)  } 
 
🤔切片元素的删除 :Go 语言中并没有提供一个内置函数将切片中的元素进行删除,但我们可以使用 [x,y] 或者 append 来实现。
1 2 3 4 5 6 7 slice := []int {1 , 2 , 3 , 4 , 5 }                  slice = slice[1 :]                              idx := 1                                        slice = append (slice[:idx], slice[idx+1 :]...)  fmt.Println(slice)  
 
⭐小结一下:
每一个切片都引用了一个底层数组。 
切片创建时存储了一个长度和一个容量,还有一个指向数组的指针,当切片添加数据时,如果没有超过容量,直接添加,超出容量自动扩容成倍增长。 
一旦切片扩容,指针会指向一个新的底层数组内存地址。 
 
2.2.8 字典 map map 是 Go 语言中内置的字典类型,存储的是键值对 key:value 类型的数据,有以下特点:
map 是完全无序 的,遍历时不会按照字母顺序或插入顺序输出,而是随机的,且只能通过 key 访问对应的 value。 
空的 slice 是可以直接使用的,因为它有底层数组;但空的 map 不能直接使用,需要先 make 或初始化后才能使用(map 是引用类型,如果声明没有初始化值,默认为 nil,是不能直接使用的)。 
map 的 key 不能重复,否则新增加的值会覆盖原来 key 对应的 value。 
 
创建 map:
1 2 3 4 5 6 var  map1 map [int ]string map2 := make (map [int ]string ) map3 := map [string ]int {"one" : 1 , "two" : 2 } 
 
使用 map:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 package  mainimport  "fmt" func  main ()   {         m := make (map [string ]int )     m["one" ] = 1      m["two" ] = 2      fmt.Println(m)                fmt.Println(len (m))           fmt.Println(m["one" ])         fmt.Println(m["unknow" ])                var  nilMap map [int ]float32           if  nilMap == nil  {         nilMap = make (map [int ]float32 )     }     nilMap[0 ] = 1.0      fmt.Println(nilMap)                r, ok := m["unknow" ]     fmt.Println(r, ok)      t, ok := m["two" ]     fmt.Println(t, ok)                delete (m, "one" )     delete (m, "two" )     fmt.Println(m)                m2 := map [string ]int {"one" : 1 , "two" : 2 }     var  m3 = map [string ]int {"one" : 1 , "two" : 2 }     fmt.Println(m2, m3)  } 
 
2.2.9 range 遍历 介绍下 range 关键字:对于 slice 或者 map,我们可以使用 range 对其进行快速遍历。
slice :第一个是索引,第二个是值 
map :第一个是键,第二个是值 
 
如果不需要索引/键,可以直接使用 _ 匿名变量代替。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 package  mainimport  (    "fmt"      "strconv"  ) 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)          }     }     fmt.Println(sum)           for  _, num := range  nums {         fmt.Println(num)     }               maps := make (map [int ]string )     maps[0 ] = "hello"      maps[1 ] = "world"      for  key, value := range  maps {                  fmt.Println(fmt.Sprintf("%d" , key) + ":"  + value)                  fmt.Println(strconv.Itoa(key) + ":"  + value)                  fmt.Println(key, ":" , value)     }          for  _, v := range  maps {         fmt.Println("v" , v)     }          for  k := range  maps {         fmt.Println("key" , k)     } } 
 
2.2.10 函数 func 💖Go 语言中的函数比较特殊,参数类型、返回值类型都是后置 的,而且函数首字母大写/小写的作用不同:
如果函数名首字母大写 则表示公共函数 ,其他包能够调用,前提得引入当前包。 
如果函数名首字母小写 则表示私有函数 ,仅能够在本包中调用。 
 
Go 语言的函数 func 结构
 
💖Golang 中的函数原生支持返回多个值 ,且在实际业务场景中都返回两个值,第一个是真正的返回结果,第二个是错误信息。
多返回值
 
接下来看看函数的基本定义及其用法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package  mainimport  "fmt" func  add (a int , b int )   (int , string ) {    return  a + b, "ok"  } 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)           v, ok := exists(map [string ]string {"a" : "A" }, "a" )     fmt.Println(v, ok)  } 
 
💖Go 语言中的函数也是一种数据结构,也可以被存储在一个变量中,调用变量的时候也就相当于调用函数——也就是可以将函数作为传递参数 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package  mainimport  "fmt" func  add (a int , b int )   (int , string ) {    return  a + b, "ok"  } func  main ()   {         f := add     ret, _ := f(2 , 3 )     fmt.Println(ret)                var  addingFunc func (int , int )   (int , string )     addingFunc = add     result, str := addingFunc(1 , 5 )     fmt.Println(result, str)  } 
 
💖Go 中定义匿名函数加上 () 相当于直接调用;如果没有 () 则表示定义一个函数,可将其赋值给变量然后进行多次调用——匿名函数 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package  mainimport  (    "fmt"      "strconv"  ) func  main ()   {         func ()   {         fmt.Println("匿名函数" )     }()                sum := func (a int , b int )   int  {         return  a + b     }(1 , 2 )     fmt.Println(sum)                myFunc := func (a, b int )   string  {         return  strconv.Itoa(a) + fmt.Sprintf("%d" , b)     }          returnVal := myFunc(6 , 66 )     fmt.Println(returnVal)  } 
 
💖甚至你可以将匿名函数作为另一个函数的参数/返回值;其中作为参数的函数叫做回调函数 ,调用的函数叫做高阶函数 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package  mainimport  "fmt" func  returnFunc (a int , b int )   func ()   int  {    return  func ()   int  {         return  a + b     } } func  increase (a int , b int )   int  {    return  a + b } func  reduce (a int , b int )   int  {    return  a - b } func  opera (a int , b int , f func (int , int )   int ) int  {    res := f(a, b)     return  res } func  main ()   {         num1 := opera(1 , 2 , increase)      num2 := opera(1 , 2 , reduce)        fmt.Println(num1, num2)               num3 := opera(3 , 4 , func (a int , b int )   int  {         return  a * b     })     fmt.Println(num3)  } 
 
💖既然 Go 语言中的函数能作为返回值和参数,自然能打造闭包结构 ,与 JS 闭包含义相同。
所谓闭包,就是一个外层函数中有内层函数,这个内层函数会操作外层函数的局部变量,并且,外层函数把内层函数作为返回值,将内层函数与外层函数中的局部变量统称为闭包结构。
💖先捋清下为什么我们需要闭包结构,闭包有什么作用?
首先看一个简单的计数器例子!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package  mainimport  "fmt" var  counter = 0 func  add ()   int  {    counter++     return  counter } func  main ()   {    add()     add()     add()     fmt.Println(counter)  } 
 
虽然我们已经达到了目的,但是任意一个函数中都可以随意改动 counter 的值,所以该计数器并不完美,那我们将 counter 放到函数中如何?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package  mainimport  "fmt" func  add ()   int  {    counter := 0      counter++     return  counter } func  main ()   {    add()     add()     ret := add()     fmt.Println(ret)  } 
 
本意想输出 3,但由于局部变量在函数每次调用时都会被初始化为 0,所以达不到预期效果。所以我们此时就需要使用闭包 来解决了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package  mainimport  "fmt" func  add ()   func ()   int  {    counter := 0      innerFunc := func ()   int  {         counter++         return  counter     }     return  innerFunc } func  main ()   {    inner := add()     inner()     inner()     ret := inner()     fmt.Println(ret)  } 
 
精简下闭包代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package  mainimport  "fmt" func  main ()   {         add := func ()   func ()   int  {         counter := 0          return  func ()   int  {             counter++             return  counter         }     }()          add()     add()     ret := add()          fmt.Println(ret)  } 
 
💖现在估计你能很轻松的理解以下代码了。
注意:由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。因此可以手动解除对内层匿名函数的引用,以便释放内存。
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 package  mainimport  "fmt" func  main ()   {    res := closure()          r1 := res()     r2 := res()          fmt.Println(res)      fmt.Println(r1)       fmt.Println(r2)                 res = nil  } func  closure ()   func ()   int  {          a := 0           return  func ()   int  {                  a++         return  a     } } 
 
💖defer 函数是 Go 语言中另一奇特的存在:当 defer 函数调用后,代码暂不执行,推迟到主函数 main 执行结束后才会执行;一般用于资源的关闭。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package  mainimport  "fmt" func  main ()   {         defer  func ()   {         fmt.Println("Close Resource" )     }()     fmt.Println("defer..." )                     } 
 
2.2.11 指针 Golang 也支持指针,用法同 C/C++,只不过支持的操作比较有限。
Go 语言中通过 & 获取变量的地址,通过 *  获取指针所对应的变量存储的数值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package  mainimport  "fmt" func  add2 (n int )   {   n += 2  } func  add2ptr (n *int )   {   *n += 2  } func  main ()   {   n := 5     add2(n)    fmt.Println(n)     add2ptr(&n)    fmt.Println(n)  } 
 
🚀接着简单介绍下:「数组指针 」、「指针数组 」、「指针函数 」、「指针参数 」
区分数组指针 和指针数组 的定义,只需看清变量名更靠近 [](指针数组) 还是 *(数组指针)。
至于要区分这二者的使用方式只需要记得 [] 优先级高于 * 即可。
 
(1)数组指针:指向数组的指针
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package  mainimport  "fmt" func  main ()   {         arr := [3 ]int {1 , 2 , 3 }     var  ars *[3 ]int      ars = &arr     (*ars)[0 ] = 0      ars[1 ] = 1      fmt.Println(ars)            fmt.Println(*ars)           fmt.Println((*ars)[1 ])  } 
 
(2)指针数组:数组元素皆为指针
1 2 3 4 5 6 7 8 9 10 11 12 13 14 a, b, c := 1 , 2 , 3  nums := [3 ]int {a, b, c} numps := [3 ]*int {&a, &b, &c} *numps[1 ] = 1  *numps[2 ] = 6  fmt.Println(nums)       fmt.Println(numps)      fmt.Println(*numps[0 ])  fmt.Println(*numps[2 ])  for  _, v := range  numps {    fmt.Print(*v, " " )  } 
 
(3)指针函数:如果一个函数返回结果是一个指针,那么这个函数就是一个指针函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package  mainimport  "fmt" func  main ()   {    var  p = pfunc()     fmt.Println((*p)[1 ])  } func  pfunc ()   *[]int  {    arr := []int {1 , 2 , 3 }     return  &arr } 
 
(4)指针参数:指针作为函数的形参
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package  mainimport  "fmt" func  main ()   {    s := 19      argpfunc(&s)     fmt.Println(s)  } func  argpfunc (p *int )   {    *p = 6  } 
 
2.2.12 结构体 & 结构体方法 Go 语言中不存在 Class 类的概念,但是可以通过结构体 struct 来实现。同时在结构体中也支持指针,避免对大结构体拷贝的开销。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 package  mainimport  "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"      e := new (user)     e.name = "wang"      e.password = "1024"           fmt.Println(a, b, c, d)      fmt.Println(e)                    fmt.Println(checkPassword(a, "996" ))      changePassword(&a, "996" )                 fmt.Println(a)                                      p := struct  {         age int          sex string          u   user     }{         age: 21 ,         sex: "Male" ,         u:   user{"w" , "1024" },     }     fmt.Println(p)  } func  checkPassword (u user, password string )   bool  {    return  u.password == password } func  changePassword (u *user, password string )   {    u.password = password } 
 
在 Golang 中还可以为结构体去定义方法,类似其他语言的类成员函数,这样就可以使用 对象.方法 去调用结构体方法了;结构体方法又分为带指针 和不带指针 两种,不带指针的是一种拷贝。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 package  mainimport  "fmt" type  user struct  {    name     string      password string  } func  (u user)   checkingPassword(password string ) bool  {    return  u.password == password } func  (u *user)   changingPassword(password string ) {    u.password = password } func  main ()   {    u := user{         name:     "w" ,         password: "1024" ,     }          isEqual := u.checkingPassword("1024" )     u.changingPassword("2022" )          fmt.Println(isEqual)      fmt.Println(u)        } 
 
2.2.13 字符串操作 Go 语言中的 strings 标准库含有很多操作字符串的工具函数,strings 主要针对 utf-8 编码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package  mainimport  (    "fmt"      "strings"  ) func  main ()   {    a := "hello"      b := "你好"      fmt.Println(strings.Contains(a, "he" ))              fmt.Println(strings.Index(a, "l" ))                  fmt.Println(strings.Count(a, "l" ))                  fmt.Println(strings.HasPrefix(a, "he" ))             fmt.Println(strings.HasSuffix(a, "lo" ))             fmt.Println(strings.Join([]string {a, b}, "-" ))      fmt.Println(strings.Repeat(a, 2 ))                   fmt.Println(strings.Replace(a, "l" , "i" , 1 ))        fmt.Println(strings.Split("a-b-c" , "-" ))            fmt.Println(strings.ToUpper(a))                     fmt.Println(strings.ToLower(a))                     fmt.Println(len (a))                                 fmt.Println(len (b))                             } 
 
2.2.14 字符串格式化 😎Go 可以使用 fmt.Printf() 或者 fmt.Sprintf() 格式化字符串(后者能将格式化后的字符串赋值给新字符串)!
😎Go 语言中额外提供了占位符 %v 来打印任意类型的变量,你还可以用 %+v、%#v 打印更加详细的信息 …
🔎字符串格式化符号|一览表 
占位符 
说明 
 
 
%d 
十进制的数字 
 
%T 
取类型 
 
%s 
取字符串 
 
%t 
取 bool 类型的值 
 
%p 
取内存地址 
 
%b 
整数以二进制显示 
 
%o 
整数以八进制显示 
 
%x 
整数以十六进制显示 
 
%v 
任意类型变量 
 
%+v 
在 %v 基础上,对结构体 字段名和值进行展开 
 
%#v 
输出 Go 语言语法格式的值 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 package  mainimport  (    "fmt"      "time"  ) type  point struct  {    x, y int  } func  main ()   {    s := "hello"      n := 123      f := 3.141592653      b := true      p := point{1 , 2 }               fmt.Printf("%s\n" , s)        fmt.Printf("%d\n" , n)        fmt.Printf("%b\n" , n)        fmt.Printf("%f\n" , f)        fmt.Printf("%.3f\n" , f)      fmt.Printf("%t\n" , b)             fmt.Printf("%T\n" , f)      fmt.Printf("%T\n" , p)           fmt.Printf("s=%v\n" , s)       fmt.Printf("n=%v\n" , n)       fmt.Printf("p=%v\n" , p)       fmt.Printf("p=%+v\n" , p)      fmt.Printf("p=%#v\n" , p)                motto := fmt.Sprintf("Today is %s, and I'm working under the %d system...\n" , time.Now(), 965 )     fmt.Printf(motto)               intVal := 1024      var  strVal string  = fmt.Sprintf("%d" , intVal)      fmt.Printf("%T" , strVal)                       } 
 
2.2.15 接口 Golang 接口内可以定义多个方法,谁将这些方法实现,就可以认为是实现了该接口(这是一种约束,不像 Java 还需要 implements 来显式实现),这样规范了方法。在调用的时候使用不同的结构体对象,可以实现执行不同的方法。这样就实现了 Go 语言中的多态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 package  mainimport  "fmt" type  action interface  {         run(int )          get() string  } type  person struct  {    name  string      speed int  } type  animal struct  {    types    string      velocity int  } func  (per *person)   run(speed int ) {    fmt.Println("Run in" , speed, "m/s" ) } func  (per *person)   get() string  {    return  per.name } func  (ani *animal)   run(velocity int ) {    fmt.Println("Run in" , velocity, "m/s" ) } func  (ani *animal)   get() string  {    return  ani.types } func  main ()   {    per := person{name: "w" , speed: 1 }     ani := animal{types: "tiger" , velocity: 6 }          var  act action     act = &per     act.run(per.speed)      name := act.get()            act = &ani     act.run(ani.velocity)      types := act.get()              fmt.Println(name, types)     } 
 
🌓Golang 还存在空接口 interface{},这种类型可以理解为任意类型,类似 Java 中的 Object 类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package  mainimport  "fmt" type  T interface  {} func  test1 (t T)   {    fmt.Println(t) } func  test2 (t interface {})   {    fmt.Println(t) } 
 
⭐既然空接口可以传递任意类型,我们就可以利用这个特性把空接口 interface{} 当作容器使用。
1 2 3 4 5 6 7 8 maps := make (map [int ]interface {}) maps[1 ] = 1  maps[3 ] = "369"  maps[6 ] = true  maps[9 ] = 9.9  fmt.Println(maps)  
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 package  mainimport  "fmt" type  Dictionary struct  {    data map [string ]interface {} } func  NewDictionary ()   *Dictionary {    return  &Dictionary{         data: make (map [string ]interface {}),     } } func  (dict *Dictionary)   Set(key string , value interface {}) {    dict.data[key] = value } func  (dict *Dictionary)   Get(key string ) interface {} {    return  dict.data[key] } func  main ()   {         dict := NewDictionary()     dict.Set("a" , "abandon" )     dict.Set("b" , 2 )     dict.Set("c" , false )          fmt.Println(dict.Get("a" ))       fmt.Println(dict.Get("d" ))   } 
 
💖更多关于空接口的解释:The Go Empty Interface Explained 
2.2.16 错误处理 💧错误和异常不同:
错误是在程序中正常存在的,可以预知的失败在意料之中。 
异常通常指在不应该出现问题的地方出现问题,比如空指针,这在人们的意料之外。 
 
在 Golang 中,错误处理通常被单独作为一个返回值以传递错误信息。不同于 Java,Go 语言能很清晰的知道是哪个函数返回了错误,并且可以使用简单的 if-else 语句加以处理。
🔎error 的定义是一个接口,接口内部包含一个返回字符串类型的方法 Error()。
1 2 3 type  error  interface  {    Error() string  } 
 
清楚 error 的定义是一个接口类型后,那么只要实现了这个接口都可以用来处理错误信息,来返回一个错误提示给用户。
Go 语言也提供了一个内置包 errors,使用 errors.New("") 来创建一个错误对象,以下为 errors 内置包中 errors.go 的定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package  errorsfunc  New (text string )   error  {   return  &errorString{text} } type  errorString struct  {   s string  } func  (e *errorString)   Error() string  {   return  e.s } 
 
⭐通常的做法是:当出错时,返回一个 nil 和一个 error;否则直接返回原有值和 nil。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package  mainimport  (    "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 ()   {    users := []user{{"w" , "1024" }, {"q" , "996" }}          u, e := findUser(users, "w" )     if  e != nil  {         fmt.Println(e)         return      }     fmt.Println(*u)           if  us, err := findUser(users, "r" ); err != nil  {         fmt.Println(err)          return      } else  {         fmt.Println(*us)     } } 
 
2.2.17 时间处理 关于时间处理,最常用的莫过于 time.now() 获取当前时间。以下还有一些 time 内置包的常见用法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 package  mainimport  (    "fmt"      "time"  ) func  main ()   {    nowTime := time.Now()     fmt.Println(nowTime)                year, month, day := time.Now().Date()     fmt.Println(year, month, day)           hour, minute, second := time.Now().Clock()     fmt.Println(hour, minute, second)                formatTime1 := nowTime.Format("2006/01/02 15:04:05" )     fmt.Println(formatTime1)      formatTime2 := nowTime.Format("2006年01月02日 15时04分05秒" )     fmt.Println(formatTime2)                created := time.Date(2022 , 5 , 9 , 11 , 12 , 13 , 0 , time.UTC)     fmt.Println(created)      fmt.Println(created.Year(), created.Month(), created.Day(),         created.Hour(), created.Minute(), created.Second())                     another := created.Add(time.Hour + time.Minute*3 )     diff := another.Sub(created)     fmt.Println(diff)                              fmt.Println(diff.Hours(), diff.Minutes())                fmt.Println(nowTime.Unix())                t, err := time.Parse("2006-01-02 15:04:05" , "2022-05-09 11:12:13" )     if  err != nil  {         panic (err)     }     fmt.Println(t == created)  } 
 
😮更多:
2.2.18 JSON 处理 Go 中操作 JSON 非常简单,对于一个结构体我们只需要确保每个字段的首字母为大写(即公开字段),那么这个结构体就能够用 json.Marshal 去序列化成一个 JSON byte[],如果要转化成字符串则通过 string() 即可;序列化后的字符串也可以用 json.Unmarshal 去反序列化到一个 struct 变量中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 package  mainimport  (    "encoding/json"      "fmt"  ) type  userInfo struct  {    Name  string      Age   int  `json:"age"`       Hobby []string  } func  main ()   {         user := userInfo{Name: "w" , Age: 21 , Hobby: []string {"Java" , "Golang" , "Python" , "C++" }}     buf, err := json.Marshal(user)      if  err != nil  {         panic (err)     }     fmt.Println(buf)              fmt.Println(string (buf))                buf, err = json.MarshalIndent(user, "" , "\t" )     if  err != nil  {         panic (err)     }     fmt.Println(string (buf))                    var  u userInfo     err = json.Unmarshal(buf, &u)     if  err != nil  {         panic (err)     }     fmt.Printf("%#v" , u)  } 
 
2.2.19 数字解析 接下来学习下字符串与数字之间的转换,在 Go 语言中,关于字符串和数字类型的转换都在 strconv 内置包中,这个包名是 string & convert 两个单词的缩写拼接而成。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 package  mainimport  (    "fmt"      "strconv"  ) func  main ()   {    f, _ := strconv.ParseFloat("1.234" , 64 )     fmt.Println(f)           n, _ := strconv.ParseInt("111" , 10 , 64 )     fmt.Println(n)           n, _ = strconv.ParseInt("0x1000" , 0 , 64 )     fmt.Println(n)                n2, _ := strconv.Atoi("123" )      fmt.Println(n2)                             var  str string  = strconv.Itoa(123 )     fmt.Println(str)           n2, err := strconv.Atoi("AAA" )     fmt.Println(n2, err)  } 
 
2.2.20 进程信息 在 Go 中,我们可以使用 os.Args 来获取程序执行时指定的命令行参数。
⭐比如我们编译一个二进制文件,执行 go run example/20-env/main.go a b c d 命令,其中有 a b c d 四个命令行参数,但是 os.Args 会是长度为 5 的 slice,因为第一个成员代表二进制自身的名字。
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package  mainimport  (    "fmt"      "os"      "os/exec"  ) func  main ()   {         fmt.Println(os.Args)      slices := os.Args     fmt.Println(len (slices))            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))  } 
 
🔎「Go 入门」中的部分图片引用自:https://juejin.cn/book/6844733833401597966 
3. Go 实战程序 上文已经介绍了 Go 语言的基础语法和一些常用标准库的使用方法,接下来通过 3 个实例真正上手 Golang!
3.1 猜谜游戏 
唯一需要注意的是 Linux/Unix 、Windows 、Mac OS  三个操作系统下换行符不一致 问题!
Linux/Unix :换行符为 \n 
Mac OS :换行符为 \r 
Windows :换行符为\r\n 
 
附部分 ASCII 码对照表
 
OK,如下就可以实现一个简单的猜谜游戏程序了!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 package  mainimport  (    "bufio"      "fmt"      "math/rand"      "os"      "strconv"      "strings"      "time"  ) func  main ()   {    maxNum := 100           rand.Seed(time.Now().UnixNano())          secretNumber := rand.Intn(maxNum)          fmt.Print("Please input your guess: " )     reader := bufio.NewReader(os.Stdin)     for  {                  input, err := reader.ReadString('\n' )          if  err != nil  {             fmt.Println("An error occured while reading input. Please try again" , err)             continue          }                                                      input = strings.TrimSuffix(input, "\r\n" )                                    guess, err := strconv.Atoi(input)         if  err != nil  {             fmt.Println("Invalid input. Please enter an integer value" )             continue          }         fmt.Println("You 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          }     } } 
 
3.2 在线词典 
以彩云小译 为例,来扒一下翻译接口:
我们需要在 Golang 中发送该请求,因为这个请求比较复杂,较难用代码构造,我们可以借助第三方工具 curlconverter    来生成代码。
首先 Copy as cURL (bash),然后借助 curlconverter 工具生成 Golang 对应的请求代码:
将代码直接运行可得到请求成功后返回的 JSON 结果(v1):
然后我们需要根据 Response Body 构造出对应的结构体,然后将响应回来的 json 字符串反序列化到结构体中,显然我们不可能自己构造(繁琐且易出错),此时需要再借助第三方工具 OKTools   生成对应的结构体。
具体做法是将彩云小译中响应的 json 字符串粘贴到 OKTools 生成对应的结构体:
构造「请求结构体」与「响应结构体」后,再次发送请求试试(v3):
可以看出所有信息都已经打印出来,但这并不是我们想要的,我们只打印 explanations 和 prons 这两部分的信息即可。
这样在线词典 就完成了,可以运行以下程序尝试一下。
3.3 SOCKS5 代理 先浅浅演示下最终效果:启动 Golang 代理服务器程序,然后通过命令行 curl -socks5 代理服务器地址 目标URL 测试(或者通过 SwitchyOmega 插件配置,然后直接访问网站),如果代理服务器能正常工作,那么 curl 命令就会正常返回,代理服务器的日志也会打印出你所访问的网站域名或者 IP,这说明我们的网络流量是通过此代理服务器转发的。
这里我们将要编写一个较为复杂的 socks5 代理服务器,虽然 socks5 协议是代理协议,但是它并不能用于出去,它的协议使用明文传输 。
该协议诞生于互联网早期,因为早些时候某些互联网的内网为了确保安全性,有很严格的防火墙策略,但是这会使其访问某些资源较为麻烦,所以 socks5 应运而生,它相当于在防火墙上开个口子,让授权用户可以通过单个端口访问内部资源。
实际上很多软件最终暴露的也是一个 socks5 协议的端口,其实爬虫中所使用的 IP 代理池中很多代理协议就是 socks5。
接着简单了解下 socks5 的工作原理(下附图解),大致流程是浏览器与 socks5 代理服务器建立 TCP 连接,然后 socks5 代理服务器再与目标服务器建立 TCP 连接,这里可分为四个阶段:握手阶段 、认证阶段 、请求阶段 、relay 阶段 。
第一阶段——握手 :浏览器向 socks5 代理服务器发起请求,其中的数据包内容包括协议版本号 VER,还有支持的认证种类 NMETHODS,以及具体的认证方法 METHOD。如果类型为 00 则表示不需要认证,如果为其他类型则进入认证流程。 
第二阶段——认证 :不作详细介绍。 
第三阶段——请求 :认证通过后浏览器会向 socks5 代理服务器发起 Connection 请求,主要信息包括版本号 VER、请求类型 CMD、保留字段 RSV、目标地址类型 ATYP、目标 IP & Port。代理服务器接收到请求后,会和目标服务器建立起连接,然后返回响应。 
第四阶段——relay :此时浏览器与目标服务器就可以通过 socks5 代理进行数据的正常收发。 
 
SOCKS5 协议工作原理
 
😮在正式实现 socks5 代理前,我们先用 Golang 实现一个简单的 TCP Echo Server  过渡一下:
💖「SOCKS5 代理服务器」完整代码(附超详细的代码注释)
🥰接着就是测试环节了,命令行测试和浏览器测试各演示一次。
命令行测试 
 
浏览器测试 :通过 SwitchyOmega  插件配置访问网站,代理服务器进行响应
 
4. 课后作业 
4.1 简化猜谜游戏 关键代码:
1 2 var  guess int _, err := fmt.Scanf("%d\r\n" , &guess)  
 
最终代码:
4.2 新增翻译引擎 所使用的翻译引擎:有道智云AI翻译 
具体操作上文已经详细介绍过了,代码如下:
🚀这里补充推荐几个翻译接口:
必应翻译 :Level 1 
火山翻译 :Level 1 
有道翻译 :Level 2(接口被加密,没法轻易破解)
salt 随机数:时间 + rand 生成 
sign:md5 加密认证 
 
 
谷歌翻译 :Level 3
 
百度翻译 :Level 3
 
 
⭐后三种翻译平台想要免费使用的话都需要破解,或者你可以氪金去申请对应翻译平台的 API 接口,比较稳定。
4.3 并行请求翻译 关键代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 func  main ()   {         var  wg sync.WaitGroup     wg.Add(2 )     go  func ()   {         queryYouDao(word)         wg.Done()     }()     go  func ()   {         queryCaiYun(word)         wg.Done()     }()     wg.Wait() } 
 
最终代码:
5. 最后 💖 如果本文对你有所帮助,点个「赞」支持一下吧!
💖/ END / 下期见!