暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

【一文Go起来】快速上手篇

牛牛码特 2021-09-13
378
概要

Golang是云原生时代的宠儿,它最大的优点在于简单有效,简单在于上手迅速、代码规范、部署方便;有效在于它能很容易写出高并发的代码,处理能力强。

Golang能适用于web后台、数据库、云原生、区块链等大多数场景,大厂与其相关的招聘岗位也在逐年增加,因此,学习Golang这样相对较新、发展前景很好的语言,我们是可以实现弯道超车的。

牛牛也秉承Golang简单、有效的理念推出一份golang学习套餐,本文是其中的快速上手篇,每个可执行代码也都附上了运行结果,希望小伙伴们读完此文,自己动手试一试,实现快速入门,用Golang开启新的旅程。

下面我们就从最基础的环境部署开始,开启我们的Golang之旅吧~


环境准备

安装Golang


Linux 安装方式


由官网的安装介绍,我们可以了解到各个系统的安装流程,对Linux来说:

1.下载安装包
下载安装包到当前目录

2.解压到指定目录
    rm -rf usr/local/go && tar -C usr/local -xzf go1.16.2.linux-amd64.tar.gz

    3.设置环境变量PATH
      export PATH=$PATH:/usr/local/go/bin

      4.检查Go版本
        go version

        可以看到,Linux安装只用下载安装包,并解压到特定目录,设置PATH环境变量之后即完成安装。

        Mac 安装

        Mac更加简单粗暴,直接下载安装包,点击安装。

        Windows安装

        Windows和Mac一样,直接点击安装包进入安装界面即可。

        Golang包官方的资源地址是在:https://golang.org/dl/,小伙伴们可以上去选择自己需要的版本,通常来说,建议是下载最新版本。

        如果暂时没有外网,又不想因此被卡住,这里牛牛也帮大家下好了目前最新版本1.14.12的包,大家可以关注公众号在后台回复【g安装包】即可获取。


        环境变量设置

        Golang有一个环境变量GOPATH,这个变量表示第三方依赖包在本地的位置,大家指定一个方便访问的路径即可。


        这样第三方依赖包都可以下载到GOPATH下面,项目也可以自动从GOPATH加载第三方依赖包。

        IDE推荐


        推荐GoLand,功能强大,开箱即用,还有完善的插件生态。习惯用vim在linux下编程的同学也请留步,GoLand可以非常方便的安装vim插件,可以同时享受IDE的强大功能和vim高效的编辑能力。

        Goland是付费软件,一般公司会提供正版给员工,如果还在学校且经济条件有限,大家可以先选30天的体验版,到30天卸载了重装


        语法介绍

        语法是任何一门语言最基础的部分,下面就让我们来看看Go的语法。


        包的概念

          package main


          import "fmt"


          func main() {
          fmt.Println("niuniumart")
          }

          输出结果

          以上代码是组成一个可执行代码最基础的三部分,换言之,每个可执行代码都必须包含Package、import以及function这三个要素。

          Golang以包来管理代码,一个目录承载一个包的内容,代码文件必须在一个包下面,比如这里我们在code目录下建了一个main.go文件,package指示代码是属于main这个包的。main函数必须要在main包下面。import用来引用外部的包,如上面示例中import引用了fmt包,就可以直接使用其方法fmt.Println

          包管理工具有三种:

          1. GOPATH:把依赖包通过go get命令拉到本地GOPATH目录下,缺点是没法实现依赖包多版本管理。

          1. DEP:将依赖包通过DEP命令打包到工程下的vendor目录。Shopee金融团队、字节跳动教育团队用的就是DEP;

          2. GoMod:将依赖包拉取到统一的pkg目录下,分版本存储。腾讯云用GoMod的团队会比较多。

          针对包管理,本文我们就不做过多扩展,后续有文章会进行专门的讲解。

          回到我们的例子,针对这个main.go文件,进行如下操作,即可运行程序:

            go build main.go 得到二进制文件main
            ./main //执行代码

            变量定义及初始化


              package main


              import "fmt"


              var globalStr string
              var globalInt int


              func main() {
              var localStr string
              var localInt int
              localStr = "first local"
              localInt = 2021
              globalInt = 1024
              globalStr = "first global"
              fmt.Printf("globalStr is %s\n", globalStr) //globalStr is first global
              fmt.Printf("globalStr is %d\n", globalInt) //globalStr is 1024
              fmt.Printf("localInt is %s\n", localStr) //localInt is first local
              fmt.Printf("localInt int is %d\n", localInt) //localInt int is 2021
              }

              输出结果

              上面的代码定义了以下四个变量:

              一个名字叫globalStr的全局字符串变量

              一个名字叫globalInt的全局整型变量

              一个名字叫localStr的局部字符串变量

              一个名字叫localInt的局部整型变量

              注意,这里的全局变量如果要在包外访问,首字母需要大写,对,你没有看错,golang是以首字母大小来区分对包外是否可见。

                package main


                import "fmt"


                func main() {
                //数组初始化
                   var strAry  = [10]string{"aa""bb""cc""dd""ee"}
                   //切片初始化
                   var sliceAry = make([]string0)
                   sliceAry = strAry[1:3]
                   //字典初始化
                   var dic = map[string]int{
                      "apple":1,
                      "watermelon":2,
                }
                   fmt.Printf("strAry %+v\n", strAry) 
                   fmt.Printf("sliceAry %+v\n", sliceAry) 
                   fmt.Printf("dic %+v\n", dic) 
                }
                 

                输出结果

                以上代码演示了数组、切片、字典的定义及初始化。可以看到切片通过索引的方式指向了数组。切片是可以更改某个元素内容的,数组则不能,在开发中,主要都是使用切片来进行逻辑处理。

                条件选择语法


                  package main


                  import "fmt"


                  func main() {
                     localStr := "case3" //是的,还可以通过 := 这种方式直接初始化基础变量
                     if localStr == "case3" {
                        fmt.Printf("into ture logic\n")
                     } else {
                        fmt.Printf("into false logic\n")
                  }
                     //字典初始化
                     var dic = map[string]int{
                        "apple":      1,
                        "watermelon"2,
                  }
                     if num, ok := dic["orange"]; ok {
                        fmt.Printf("orange num %d\n", num)
                     }
                     if num, ok := dic["watermelon"]; ok {
                        fmt.Printf("watermelon num %d\n", num)
                  }
                     switch localStr {
                     case "case1":
                        fmt.Println("case1")
                     case "case2":
                        fmt.Println("case2")
                     case "case3":
                        fmt.Println("case3")
                     default:
                        fmt.Println("default")
                  }
                  }

                  输出结果

                  if语句在Golang和其他语言中的表现形式一样,没啥区别。上面的例子同时也展示了用if判断某个key在map是否为空的写法。

                  switch中,每个case都默认break。即如果是case1,那么执行完之后,就会跳出switch条件选择。如果希望从某个case顺序往下执行,可以使用fallthrough关键字。

                  循环写法


                    package main


                    import "fmt"


                    func main() {
                      for i := 0; i < 5; i++ {
                         fmt.Printf("current i %d\n", i)
                    }
                      j := 0
                      for {
                         if j == 5 {
                            break
                    }
                         fmt.Printf("current j %d\n", j)
                         j++
                    }
                      var strAry = []string{"aa""bb""cc""dd""ee"//是的,不指定初始个数也ok
                      //切片初始化
                      var sliceAry = make([]string0)
                      sliceAry = strAry[1:3]
                      for i, str := range sliceAry {
                         fmt.Printf("slice i %d, str %s\n", i, str)
                    }
                      //字典初始化
                      var dic = map[string]int{
                         "apple":      1,
                         "watermelon"2,
                    }
                      for k, v := range dic {
                         fmt.Printf("key %s, value %d\n", k, v)
                    }
                    }

                    输出结果

                    语言特性

                    协程(goroutine


                    协程是Golang最重要的一个特性。

                    在协程出现之前,线程被作为调度的最小单位。协程可以理解是一种用户态,逻辑层面的线程。

                    通过协程,我们将很容易地实现高并发:假如你要做三件事,假设要执行a,b,c三个方法。代码该怎么写?平常我们的写法就是:

                      package main


                      import (
                      "fmt"
                      "time"
                      )


                      func a() {
                      time.Sleep(3 * time.Second)
                      fmt.Println("it's a")
                      }
                      func b() {
                      time.Sleep(2 * time.Second)
                      fmt.Println("it's b")
                      }
                      func c() {
                      time.Sleep(1 * time.Second)
                      fmt.Println("it's c")
                      }
                      func main() {
                      a()
                      b()
                      c()
                        time.Sleep(1 * time.Second)
                      }

                      输出结果

                      以上的代码只有a做完了,才能做b,b做完了,才能做c。

                      但Golang语言层面支持协程,通过关键字go,后面跟一个方法,就生成了一个协程:

                        package main


                        import (
                        "fmt"
                        "time"
                        )


                        func a() {
                        time.Sleep(3 * time.Second)
                        fmt.Println("it's a")
                        }
                        func b() {
                        time.Sleep(2 * time.Second)
                        fmt.Println("it's b")
                        }
                        func c() {
                        time.Sleep(1 * time.Second)
                        fmt.Println("it's c")
                        }
                        func main() {
                        go a()
                        go b()
                        go c()
                        time.Sleep(5 * time.Second)
                        }

                        输出结果

                        在协程里,三个方法就可以并发进行,可以看到,由于方法a执行时间最久,所以最后才打印。协程Golang运行时调度,是充分利用了Golang多核的性能。后续文章牛牛会专门深入讲解协程的原理,我们现在作为入门者,只需要会使用它即可。

                        小伙伴们也可以想想,牛牛为何要在a,b,c三个方法之后还要sleep5秒,这里先留个悬念。

                        通道(channel


                        通道的要点:

                        1.类似unix中管道(pipe),先进先出;

                        2.线程安全,多个goroutine同时访问,不需要加锁;

                        3.channel是有类型的,一个整数的channel只能存放整数。

                        通道的定义:

                          var ch0 chan int
                          var ch1 chan string
                          var ch2 chan map[string]string


                          type stu struct{}


                          var ch3 chan stu
                          var ch4 chan *stu

                          通道可以用于协程之间数据的传递,一般分为有缓冲通道无缓冲通道

                          两个协程间如果有数据交流怎么办?这时候就可以用通道来传递。Golang的设计思想就是用通信代替共享内存。

                            package main


                            import (
                            "fmt"
                            "time"
                            )


                            var ch chan int


                            func a() {
                            time.Sleep(3 * time.Second)
                            a := 5
                            ch <- a
                            fmt.Println("out of a")
                            }
                            func b() {
                            time.Sleep(1 * time.Second)
                            fromA := <- ch
                            b := fromA + 3
                            fmt.Println("b is ", b)
                            }
                            func main() {
                            ch = make(chan int, 1)
                            go a()
                            go b()
                            time.Sleep(20 * time.Second)
                            fmt.Println("out of main")
                            }

                            输出结果

                            可以看到,更慢一些的b,是等管道有数据才继续运行,并成功拿到了a往管道里放入的数字5!这就完成了协程间的通信。

                            另外,这里也涉及到一个面试高频问题:有缓冲和无缓冲通道的区别?

                            通道可以带缓冲,就是说可以往通道里放多个数据,放满了,才会阻塞。

                            有一段时间,牛牛一直误以为无缓冲通道就是容量为1的有缓冲通道,于是就以此为例来进行讲解:

                              chSync := make(chan int) //无缓冲
                              chAsyn := make(chan int,1) //有缓冲

                              同样是向通道里塞一个数据:chSync <-1

                              无缓冲场景:一直要等有别的协程通过<-chSync接手了这个参数,那么chSync<-1才会继续下去,要不然就一直阻塞着。

                              有缓冲场景:chAsyn<-1则不会阻塞,因为缓冲大小是1,只有当放第二个值的时候,第一个还没被人拿走,这时候才会阻塞。

                              仔细理解下,实际这就是同步和异步的区别,无缓冲一定是同步等待,有缓冲只有在缓冲满了,异步又处理不过来的时候,才会阻塞。

                              无缓冲🌰

                                package main


                                import (
                                "fmt"
                                "time"
                                )


                                var ch chan int


                                func a() {
                                time.Sleep(3 * time.Second)
                                a := 5
                                ch <- a
                                fmt.Println("out of a")
                                }
                                func b() {
                                  time.Sleep(1 * time.Second)
                                }
                                func main() {
                                  ch = make(chan int//无缓冲管道
                                go a()
                                go b()
                                time.Sleep(20 * time.Second)
                                  fmt.Println("out of main")
                                }

                                输出结果

                                可以看到,在没有接盘侠的情况下,a在写管道时被阻塞了。

                                有缓冲🌰

                                  package main


                                  import (
                                  "fmt"
                                  "time"
                                  )


                                  var ch chan int


                                  func a() {
                                  time.Sleep(3 * time.Second)
                                  a := 5
                                  ch <- a
                                  fmt.Println("out of a")
                                  }
                                  func b() {
                                    time.Sleep(1 * time.Second)
                                  }
                                  func main() {
                                  ch = make(chan int, 1)
                                  go a()
                                  go b()
                                  time.Sleep(20 * time.Second)
                                  fmt.Println("out of main")
                                  }

                                  输出结果

                                  可以看到,函数a往管道写入一个数据,即使没有消费者,也并未阻塞。

                                  接口( interface)


                                  Go 语言提供了一种特别的数据类型——接口,它把所有具有共性的方法定义在一起,任何其他类型只要实现了这些方法就是实现了这个接口。


                                  话不多说,看看🌰:


                                    package main


                                    import "fmt"


                                    type Shape interface {
                                     Area() float64
                                     Perimeter() float64
                                    }
                                    // type rect
                                    type Rect struct {
                                     height float64
                                     weight float64
                                    }
                                    func (p *Rect) Area() float64 {
                                      return p.height * p.weight
                                    }
                                    func (p *Rect) Perimeter() float64 {
                                     return 2 * (p.height + p.weight)
                                    }
                                    func main() {
                                     var s Shape = &Rect{height:10, weight:8}
                                     fmt.Println(s.Area())
                                     fmt.Println(s.Perimeter())
                                    }

                                    输出结果

                                    代码中Shape就是一个接口,声明了两个方法:面积(Area)和周长(Perimeter)。

                                    咱们定义了一个具体结构Rect,实现这个接口。可以看到,用基础的Shape接口,可以一个指向Rect对象,并调用其方法。

                                    接口提供了面向对象编程的能力,如果你掌握多种语言,比如Golang、C++、Java等等,那么一定会问Golang的多态和C++的多态有什么区别(使用相同类型的引用,指向不同类型对象,即多态)。

                                    答案就是C++或者Java是需要主动声明基础类,而Golang,只需要实现某个interface的全部方法,那么就是实现了该类型。所以,Golang的继承关系是非侵入式的,这也是Golang的特色与优点。


                                    单元测试介绍

                                    为了保证代码的质量,很多公司都会要求写单元测试。这里介绍单元测试的两个常用指标:

                                    1. 函数覆盖率:被调用到的函数个数/总函数个数,通常要求100%;

                                    2. 行覆盖率:被调用到的行数/总行数,通常要求>60%。

                                    通过单元测试,我们可以针对不同场景测试代码,是研发自己对质量的把控。

                                    牛牛之前在字节跳动SaaS化部门,没有专门的测试人员,对单元测试的要求就非常高,行覆盖率需要达到80%。


                                    go test


                                    • go的test一般以xxx_test.go为文件名,xxx并没有特别要求必须是要实测的文件名;


                                    • TestMain作为初始化test;

                                    • Testxxx(t* testing.T);

                                    • go test即可运行单元测试;

                                    • go test --v fileName --test.run funcName可以指定单测某个方法。

                                    我们来创建一个main_test.go文件进行示例,main.go文件就使用上面的interface例子,包结构如下:

                                      ├── main.go
                                      ├── main_test.go
                                        package main


                                        import (
                                        "testing"
                                        )


                                        func TestRect(t *testing.T) {
                                          var s Shape = &Rect{height:10, weight:8}
                                        if s.Area() != 80 {
                                        t.Errorf("area %f\n", s.Area())
                                        }
                                        if s.Perimeter() != 30 {
                                        t.Errorf("perimeter %f\n", s.Perimeter())
                                        }
                                        }


                                        使用go test--v main.go--test.run TestRect

                                        于周长Perimeter不符合预期,则会有如下提示:

                                        不同编辑器输出结果会有些许不同

                                        go convey


                                        go convey可以很好的支持setup和teardown,它可以在运行单个测试用例前都进行一次状态初始化,在结束后再进行销毁。这样如果有多个子用例,可以复用同一套初始化环境。

                                        go convey还有很多已经定义好,能够直接使用的assert函数,并且还可以自定义assert函数。

                                        常用的assert如下:
                                          var (
                                          ShouldEqual = assertions.ShouldEqual
                                          ShouldNotEqual = assertions.ShouldNotEqual
                                          ShouldBeGreaterThan = assertions.ShouldBeGreaterThan
                                          ShouldBeGreaterThanOrEqualTo = assertions.ShouldBeGreaterThanOrEqualTo
                                          ShouldBeLessThan = assertions.ShouldBeLessThan
                                          ShouldBeLessThanOrEqualTo = assertions.ShouldBeLessThanOrEqualTo
                                          ShouldBeBetween = assertions.ShouldBeBetween
                                          ShouldNotBeBetween = assertions.ShouldNotBeBetween
                                          ShouldBeBetweenOrEqual = assertions.ShouldBeBetweenOrEqual
                                          ShouldNotBeBetweenOrEqual = assertions.ShouldNotBeBetweenOrEqual
                                          ShouldContainSubstring = assertions.ShouldContainSubstring
                                          ShouldNotContainSubstring = assertions.ShouldNotContainSubstring
                                          ShouldPanic = assertions.ShouldPanic
                                          ShouldBeError = assertions.ShouldBeError
                                          )

                                          使用举例:

                                            package main


                                            import (
                                            "testing"


                                            "github.com/smartystreets/goconvey/convey"
                                            )


                                            func TestRect(t *testing.T) {
                                            convey.Convey("TestRect", t, func() {
                                            var s Shape = &Rect{height: 10, weight: 8}
                                            convey.So(s.Area(), convey.ShouldEqual, 80)
                                            convey.So(s.Perimeter(), convey.ShouldEqual, 30)
                                            })
                                            }

                                            由于Perimeter不符合预期,会出现如下提示:

                                            输出结果


                                            用convey做断言,是不是更清晰明了了。

                                            用ORM连接数据库


                                            什么是ORM?


                                            ORM的全称是:Object Relational Mapping(对象关系映射),其主要作用是在编程中,把面向对象的概念跟数据库中表的概念对应起来。

                                            举例来说就是我们定义一个对象,那就对应着一张表,这个对象的实例,就对应着表中的一条记录。

                                            GORM使用示例:

                                              package main


                                              import (
                                              "fmt"
                                                 "github.com/jinzhu/gorm" 
                                              _ "github.com/jinzhu/gorm/dialects/mysql"
                                              )


                                              type User struct {
                                              Name string
                                              Age int
                                              }


                                              func main() {
                                              username := ""
                                              pwd := ""
                                                  addr := "" //ip:port
                                              database := ""
                                              args := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8&parseTime=True&loc=Local", username, pwd, addr, database)
                                              // step1 : 连接数据库
                                              db, err := gorm.Open("mysql", args)
                                              if err != nil {
                                              fmt.Println(err)
                                              //do something
                                              return
                                              }
                                              defer db.Close()
                                              // step2 : 插入一行记录
                                              user := User{Name: "niuniu", Age: 18}
                                              err = db.Create(&user)
                                              if err != nil {
                                              fmt.Println(err)
                                              return
                                              }
                                              // step3 :查询记录
                                              var tmpUser User
                                              err = db.Where("name = ?", "niuniu").First(&tmpUser).Error //查询User并将信息保存到tmpUser
                                              if err != nil {
                                              fmt.Println(err)
                                              return
                                              }
                                              fmt.Println(tmpUser)
                                              }

                                              输出结果


                                              以一个web server结束


                                              最简化样例


                                              Golang http server有几种写法,这里介绍最简单一种,让我们看看到底有多简单:这里我们实现一个SayHello接口,访问该接口,会以“hello"字符串回包。

                                                package main


                                                import (
                                                "log"
                                                "net/http"
                                                )


                                                func SayHello(w http.ResponseWriter, r *http.Request) {
                                                w.Write([]byte("hello")) //以字符串"hello"作为返回包
                                                }


                                                func main() {
                                                http.HandleFunc("/say_hello", SayHello)
                                                err := http.ListenAndServe(":8080", nil) //开启一个http服务
                                                if err != nil {
                                                log.Print("ListenAndServe: ", err)
                                                return
                                                }
                                                }


                                                用框架来一发


                                                在实际开发中,很少会直接用http裸写sever,因为如果进行功能的完善,比如可插拔中间件实现,最终就是自己实现了框架,而实际开发中,我们会选择久经考验的完善框架,比如gin:

                                                  package main


                                                  import (
                                                  "github.com/gin-gonic/gin"
                                                  "log"
                                                  "net/http"
                                                  )


                                                  func SayHello(c *gin.Context) {
                                                  c.String(http.StatusOK, "hello") //以字符串"hello"作为返回包
                                                  }


                                                  func main() {
                                                  engine := gin.Default() //生成一个默认的gin引擎
                                                  engine.Handle(http.MethodGet,"/say_hello", SayHello)
                                                     err := engine.Run(":8080"//使用8080端口号,开启一个web服务
                                                  if err != nil {
                                                  log.Print("engine run err: ", err.Error())
                                                  return
                                                  }
                                                  }
                                                  让我们通过浏览器看看成果~


                                                  小结

                                                  至此,Golang的基本玩法,大家有所了解了吗?

                                                  希望Go起来这个系列的文章可以帮助大家快速入门,尽快投入开发,但如果要成为资深的Golang开发者,还需要针对细节,做深入研究。

                                                  如果大家对Go语言还有什么疑问或者想要牛牛深入分析它的哪一方面,欢迎在评论区留言告诉牛牛哦!牛牛在接下来的文章里也会一一解答的~


                                                  END
                                                  关注公众号,免费领取学习资料
                                                  你好,我是牛牛,普通二本毕业。
                                                  本科进腾讯,去过外企,肝过头条。
                                                  目前回腾讯窝着。
                                                  分享我的故事,期待与你一同成长!


                                                  点个“赞”和“在看”鼓励一下嘛~


                                                  文章转载自牛牛码特,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                                                  评论