• Golang快速入门:从菜鸟到大佬


    最近写了不少Go代码,但是写着写着,还是容易忘,尤其是再写点Python代码后。所以找了一篇不错的Golang基础教程,翻译一下,时常看看。

    原文链接: 「Learning Go — from zero to hero」 by Milap Neupane

    开始

    Go是由各种 包 组成的。main包是程序的入口,由它告诉编译器,这是一个可执行程序,而不是共享包。main包定义如下:

    package main
    

    工作区

    Go的工作区是由环境变量GOPATH决定的。
    你可以在工作区里随心所欲地写代码,Go会在GOPATH或者GOROOT目录下搜索包。注:GOROOT是Go的安装路径。

    设置GOPATH为你想要的目录:

    # export 环境变量
    export GOPATH=~/workspace
    # 进入工作区目录
    cd ~/workspace
    

    在工作区目录里创建main.go文件。

    package main
    
    import (
     "fmt"
    )
    
    func main(){
      fmt.Println("Hello World!")
    }
    

    我们使用import关键字来引入一个包。func main是执行代码的入口,fmt是Go的内置包,主要用来格式化输入/输出。而Printlnfmt中的一个打印函数。

    想要运行Go程序,有两种方法。

    方法一

    大家都知道,Go是一门编译型语言,所以在执行之前,我们需要先编译它。

    > go build main.go
    

    这个命令会生成二进制可执行文件 main,然后我们再运行它。

    > ./main 
    # Hello World!
    

    方法二

    一个go run命令就可以搞定。

    go run main.go
    # Hello World!
    

    注意:你可以在这个网站执行本文中的代码。

    变量

    Go中的变量都是显式声明的。Go是静态语言,因此声明变量时,就会去检查变量的类型。

    变量声明有以下三种方式。

    # 1) a的默认值为0
    var a int
    
    # 2) 声明并初始化a,a自动赋值为int
    var a = 1
    
    # 3) 简写声明
    message := "hello world"
    

    还可以在一行声明多个变量

    var b, c int = 2, 3
    

    数据类型

    数字,字符串 和 布尔型

    Go 支持的数字存储类型有很多,比如 int, int8, int16, int32, int64,uint, uint8, uint16, uint32, uint64, uintptr 等等。

    字符串类型存储一个字节序列。使用string关键字来声明。

    布尔型使用bool声明。

    Go还支持复数类型数据类型,可以使用complex64complex128进行声明。

    var a bool = true
    var b int = 1
    var c string = 'hello world'
    var d float32 = 1.222
    var x complex128 = cmplx.Sqrt(-5 + 12i)
    

    数组, 分片 和 映射Map

    数组是包含同一数据类型的元素序列,在声明时确定数组长度,因此不能随意扩展。

    数组的声明方式如下:

    var a [5]int
    

    多维数组的声明方式如下:

    var multiD [2][3]int
    

    Go中的数组有一定限制,比如不能修改数组长度、不能添加元素、不能获取子数组。这时候,更适合使用slice[分片]这一类型。

    分片用于存储一组元素,允许随时扩展其长度。分片的声明类似数组,只是去掉了长度声明。

    var b []int
    

    这行代码会创建一个 0容量、0长度的分片。也可以使用以下代码 设置分片的容量和长度。

    // 初始化一个长度为5,容量为10的分片
    numbers := make([]int,5,10)
    

    实际上,分片是对数组的抽象。分片使用数组作为底层结构。一个分片由三部分组成:容量、长度和指向底层数组的指针。

    使用append或者copy方法可以扩大分片的容量。append方法在分片的末尾追加元素,必要时会扩大分片容量。

    numbers = append(numbers, 1, 2, 3, 4)
    

    还可以使用copy方法来扩大容量。

    // 创建一个更大容量的分片
    number2 := make([]int, 15)
    // 把原分片复制到新分片
    copy(number2, number)
    

    如何创建一个分片的子分片呢?参考以下代码。

    // 创建一个长度为4的分片
    number2 = []int{1,2,3,4}
    fmt.Println(numbers) // -> [1 2 3 4]
    // 创建子分片
    slice1 := number2[2:]
    fmt.Println(slice1) // -> [3 4]
    slice2 := number2[:3]
    fmt.Println(slice2) // -> [1 2 3]
    slice3 := number2[1:4]
    fmt.Println(slice3) // -> [2 3 4]
    

    Map也是Go的一种数据类型,用于记录键值间的映射关系。使用以下代码创建一个map。

    var m map[string]int
    
    // 新增 键/值
    m['clearity'] = 2
    m['simplicity'] = 3
    // 打印值
    fmt.Println(m['clearity']) // -> 2
    fmt.Println(m['simplicity']) // -> 3
    

    这里,m是一个键为string,值为int的map变量。

    类型转换

    接下来看一下如何进行简单的类型转换。

    a := 1.1
    b := int(a)
    fmt.Println(b)
    //-> 1
    

    并非所有的数据类型都能转换成其他类型。注意:确保数据类型与转换类型相互兼容。

    条件语句

    if else

    参考以下代码中的if-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")
    }
    

    switch case

    switch-case用于组织多个条件语句,详看以下代码

    i := 2
    switch i {
    case 1:
     fmt.Println("one")
    case 2:
     fmt.Println("two")
    default:
     fmt.Println("none")
    }
    

    循环

    Go中用于循环的关键字只有一个for

    i := 0
    sum := 0
    for i < 10 {
     sum += 1
      i++
    }
    fmt.Println(sum)
    

    以上代码类似于C语言中的while循环。另一种循环方式如下:

    sum := 0
    for i := 0; i < 10; i++ {
      sum += i
    }
    fmt.Println(sum)
    

    Go中的死循环

    for {
    }
    

    指针

    Go提供了指针,用于存储值的地址。指针使用*来声明。

    var ap *int
    

    这里的ap变量即指向整型的指针。使用&运算符获取变量地址,*运算符用来获取指针所指向的值。

    a := 12
    ap = &a
    
    fmt.Println(*ap)
    // => 12
    

    以下两种情况,通常优先选用指针。

    • 把结构体作为参数传递时。因为值传递会耗费更多内存。
    • 声明某类型的方法时。传递指针后,方法/函数可以直接修改指针所指向的值。

    比如:

    func increment(i *int) {
      *i++
    }
    func main() {
      i := 10
      increment(&i)
      fmt.Println(i)
    }
    //=> 11
    

    函数

    main包中的main函数是go程序执行的入口,除此以外,我们还可以定义其他函数。

    func add(a int, b int) int {
     c := a + b
     return c
    }
    func main() {
     fmt.Println(add(2, 1))
    }
    //=> 3
    

    如上所示,Go中使用func关键字加上函数名来定义一个函数。函数的参数需要指明数据类型,最后是返回的数据类型。

    函数的返回值也可以在函数中提前定义:

    func add(a int, b int) (c int) {
      c = a + b
      return
    }
    func main() {
      fmt.Println(add(2, 1))
    }
    //=> 3
    

    这里c被定义为返回值,因此调用return语句时,c会被自动返回。

    你也可以一次返回多个变量:

    func add(a int, b int) (int, string) {
      c := a + b
      return c, "successfully added"
    }
    func main() {
      sum, message := add(2, 1)
      fmt.Println(message)
      fmt.Println(sum)
    }
    

    方法、结构体和接口

    Go 不是完全面向对象的语言,但是有了 方法、结构体和接口,它也可以达到面向对象的效果。

    Struct 结构体

    结构体包含不同类型的字段,可用来对数据进行分组。例如,如果我们要对Person类型的数据进行分组,那么可以定义一个人的各种属性,包括姓名,年龄,性别等。

    type person struct {
      name string
      age int
      gender string
    }
    

    有了Person类型后,现在来创建一个 Person对象:

    //方法 1: 指定参数和值
    p = person{name: "Bob", age: 42, gender: "Male"}
    
    //方法 2: 仅指定值
    person{"Bob", 42, "Male"}
    

    可以使用.来获取一个对象的参数。

    p.name
    //=> Bob
    p.age
    //=> 42
    p.gender
    //=> Male
    

    也可以通过结构体的指针对象来获取参数。

    pp = &person{name: "Bob", age: 42, gender: "Male"}
    pp.name
    //=> Bob
    

    方法

    方法是一种带有接收器的函数。接收器可以是一个值或指针。我们可以把刚刚创建的Person类型作为接收器来创建方法:

    package main
    import "fmt"
    
    // 定义结构体
    type person struct {
      name   string
      age    int
      gender string
    }
    
    // 定义方法
    func (p *person) describe() {
      fmt.Printf("%v is %v years old.", p.name, p.age)
    }
    func (p *person) setAge(age int) {
      p.age = age
    }
    
    func (p person) setName(name string) {
      p.name = name
    }
    
    func main() {
      pp := &person{name: "Bob", age: 42, gender: "Male"}
      
      // 使用 . 来调用方法 
      pp.describe()
      // => Bob is 42 years old
      pp.setAge(45)
      fmt.Println(pp.age)
      //=> 45
      pp.setName("Hari")
      fmt.Println(pp.name)
      //=> Bob
    }
    

    注意,此处的接收器是一个指针,方法中对指针进行的任何修改,都可以反映在接收器pp上。这样可以避免复制带来的内存消耗。

    注意:上面示例中,age被修改了,而name不变。因为只有setAge传入的是指针类型,可以对接收器进行修改。

    接口

    在Go中,接口是方法的集合。接口可以对一个类型的属性进行分组,比如:

    type animal interface {
      description() string
    }
    

    animal是一个接口。通过实现animal接口,我们来创建两种不同类型的动物。

    package main
    
    import (
      "fmt"
    )
    
    type animal interface {
      description() string
    }
    
    type cat struct {
      Type  string
      Sound string
    }
    
    type snake struct {
      Type      string
      Poisonous bool
    }
    
    func (s snake) description() string {
      return fmt.Sprintf("Poisonous: %v", s.Poisonous)
    }
    
    func (c cat) description() string {
      return fmt.Sprintf("Sound: %v", c.Sound)
    }
    
    func main() {
      var a animal
      a = snake{Poisonous: true}
      fmt.Println(a.description())
      a = cat{Sound: "Meow!!!"}
      fmt.Println(a.description())
    }
    
    //=> Poisonous: true
    //=> Sound: Meow!!!
    

    在main函数中,我们创建了一个类型为animal的变量a。然后,给动物指定蛇和猫的类型,并打印a.description

    在Go中,所有的代码都写在包里面。main包是程序执行的入口,Go自带了很多内置包,最有名的就是刚刚用过的fmt包。

    “Go packages in the main mechanism for programming in the large that go provides and they make possible to divvy up a large project into smaller pieces.”

    — Robert Griesemer

    安装一个包

    go get <package-url-github>
    // 举个栗子
    go get github.com/satori/go.uuid
    

    包默认安装在GOPATH环境变量设置的工作区中。可以使用cd $GOPATH/pkg命令进入目录,查看已安装的包。

    自定义包

    首先创建一个custom_package文件夹

    > mkdir custom_package
    > cd custom_package
    

    假设要创建一个person包,首先在custom_package目录下创建一个person文件夹。

    > mkdir person
    > cd person
    

    然后创建一个 person.go文件

    package person
    func Description(name string) string {
      return "The person name is: " + name
    }
    func secretName(name string) string {
      return "Do not share"
    }
    

    现在需要安装这个包,以便引入并使用它。

    > go install
    

    注意:如果以上命令报错,确认一下GO111MODULE环境变量是否设置正确,参考链接

    然后回到custom_package目录下,创建一个main.go文件。

    package main
    import(
      "custom_package/person"
      "fmt"
    )
    func main(){ 
      p := person.Description("Milap")
      fmt.Println(p)
    }
    // => The person name is: Milap
    

    现在,就可以引入包,并调用Description方法了。注意,secretName方法是小写字母开头的私有方法,所以不能被外部调用。

    包的文档

    Go内置了对包文档的支持。运行以下命令生成文档:

    go doc person Description
    

    这将为person包生成Description函数的文档。请使用以下命令运行Web服务器,查看文档:

    godoc -http=":8080"
    

    打开这个链接http://localhost:8080/pkg/,就能看到文档了。

    Go中的一些内置包

    fmt

    fmt包实现了格式化I/O功能。我们已经使用过这个包打印内容到标准输出流了。

    json

    另外一个很有用的包是json,用来编码/解码Json数据。

    // 编码
    package main
    
    import (
      "fmt"
      "encoding/json"
    )
    
    func main(){
      mapA := map[string]int{"apple": 5, "lettuce": 7}
      mapB, _ := json.Marshal(mapA)
      fmt.Println(string(mapB))
    }
    
    // 解码
    package main
    
    import (
      "fmt"
      "encoding/json"
    )
    
    type response struct {
      PageNumber int `json:"page"`
      Fruits []string `json:"fruits"`
    }
    
    func main(){
      str := `{"page": 1, "fruits": ["apple", "peach"]}`
      res := response{}
      json.Unmarshal([]byte(str), &res)
      fmt.Println(res.PageNumber)
    }
    //=> 1
    

    使用Unmarshal解码json字节时,第一个参数是json字节,第二个是期望解码后的结构体指针。注意:json:"page"负责把page映射到结构体中的PageNumber字段上。

    错误处理

    报错是程序中的意外产物。假如我们正在使用API调用一个外部服务。这个API调用可能成功,也可能失败。比如,可以使用以下方法,处理报错:

    package main
    
    import (
      "fmt"
      "net/http"
    )
    
    func main(){
      resp, err := http.Get("http://example.com/")
      if err != nil {
        fmt.Println(err)
        return
      }
      fmt.Println(resp)
    }
    

    返回自定义错误

    在写函数时,我们可能会遇到需要报错的情景,这时可以返回一个自定义的error对象。

    func Increment(n int) (int, error) {
      if n < 0 {
        // return error object
        return nil, errors.New("math: cannot process negative number")
      }
      return (n + 1), nil
    }
    func main() {
      num := 5
     
      if inc, err := Increment(num); err != nil {
        fmt.Printf("Failed Number: %v, error message: %v", num, err)
      }else {
        fmt.Printf("Incremented Number: %v", inc)
      }
    }
    

    大部分的内置包或者外部包,都有自己的报错处理机制。因此我们使用的任何函数可能报错,这些报错都不应该被忽略,应该像上面示例中,在调用函数的地方,优雅地处理报错。

    Panic

    当程序在运行过程中,突然遇到了未处理的报错,就会导致panic。在Go中,更推荐使用error对象,而不是panic来处理异常。发生panic后,程序会停止运行,但会运行defer语句代码。

    //Go
    package main
    
    import "fmt"
    
    func main() {
        f()
        fmt.Println("Returned normally from f.")
    }
    
    func f() {
        defer func() {
            if r := recover(); r != nil {
                fmt.Println("Recovered in f", r)
            }
        }()
        fmt.Println("Calling g.")
        g(0)
        fmt.Println("Returned normally from g.")
    }
    
    func g(i int) {
        if i > 3 {
            fmt.Println("Panicking!")
            panic(fmt.Sprintf("%v", i))
        }
        defer fmt.Println("Defer in g", i)
        fmt.Println("Printing in g", i)
        g(i + 1)
    }
    

    Defer

    Defer语句总是在函数最后执行。

    在上面的栗子中,我们触发了panic,但是defer语句依然会在最后执行。Defer适用于 需要在函数最后执行某些操作的场景,比如关闭文件。

    并发

    Go在设计时考虑了并发性。 Go中的并发可以通过轻量级线程Go routines来实现。

    Go routine

    Go routine是一个函数,它可以与另一个函数并行或并发执行。 创建Go routine非常简单,只需在函数前面添加关键字go,就可以使其并行执行。 同时,它很轻量级,因此可以创建上千个routine

    package main
    import (
      "fmt"
      "time"
    )
    func main() {
      go c()
      fmt.Println("I am main")
      time.Sleep(time.Second * 2)
    }
    func c() {
      time.Sleep(time.Second * 2)
      fmt.Println("I am concurrent")
    }
    //=> I am main
    //=> I am concurrent
    

    上面的示例中,c函数是一个Go routine,与main函数中的线程并行。有时我们想在多个线程之间共享资源。 Go倾向于不与另一个线程共享变量,因为这会增加死锁和资源等待的可能。但是仙人自有妙招,就是接下来讲到的go channel

    Channels

    我们可以使用channel在两个routine之间传递数据。创建channel时,需要指定其接收的数据类型。

    c := make(chan string)
    

    通过上面创建的channel,我们可以发送/接收string类型的数据。

    package main
    
    import "fmt"
    
    func main(){
      c := make(chan string)
      go func(){ c <- "hello" }()
      msg := <-c
      fmt.Println(msg)
    }
    //=>"hello"
    

    接收方channel会一直等待发送方发数据到channel

    单向channel

    在某些场景下,我们希望Go routine只接收数据但不发送数据,反之亦然。 这时,我们可以创建一个单向channel

    package main
    
    import (
     "fmt"
    )
    
    func main() {
     ch := make(chan string)
     
     go sc(ch)
     fmt.Println(<-ch)
    }
    
    // sc函数:只能发送数据给 channel,不能接收数据
    func sc(ch chan<- string) {
     ch <- "hello"
    }
    

    使用select语句在Go routine中处理多个channel

    一个函数可能正在等待多个通道。这时,我们可以使用select语句。

    package main
    
    import (
     "fmt"
     "time"
    )
    
    func main() {
     c1 := make(chan string)
     c2 := make(chan string)
     go speed1(c1)
     go speed2(c2)
     fmt.Println("The first to arrive is:")
     select {
     case s1 := <-c1:
      fmt.Println(s1)
     case s2 := <-c2:
      fmt.Println(s2)
     }
    }
    
    func speed1(ch chan string) {
     time.Sleep(2 * time.Second)
     ch <- "speed 1"
    }
    
    func speed2(ch chan string) {
     time.Sleep(1 * time.Second)
     ch <- "speed 2"
    }
    // => The first to arrive is:
    // => speed 2
    

    Buffered channel

    在Go中,你还可以使用缓冲区channel,如果缓冲区已满,发送到该channel的消息将被阻塞。

    package main
    
    import "fmt"
    
    func main(){
      ch := make(chan string, 2)
      ch <- "hello"
      ch <- "world"
      ch <- "!" // extra message in buffer
      fmt.Println(<-ch)
    }
    
    // => fatal error: all goroutines are asleep - deadlock!
    

    最后唠唠嗑

    为什么 Golang 能够成功呢?

    Simplicity… — Rob-pike

    因为简单...

    好了,本文终于结束了!你从菜鸟变成大佬了吗?开个玩笑,希望看完能有所收获。

  • 相关阅读:
    软件上线标准
    rap使用手册
    微服务
    什么是集合?
    什么是maven?maven中的pom文件是做什么的?
    什么是连接池?
    架构
    什么是反射?
    产品
    描述下fastJSON,jackson等等的技术
  • 原文地址:https://www.cnblogs.com/wwwn/p/12804506.html
Copyright © 2020-2023  润新知