作者:Milap Neupane

链接:https://medium.freecodecamp.org/learning-go-from-zero-to-hero-d2a3223b3d86


函数

  main.go包中定义的func main()是执行程序的入口。可以定义和使用更多函数。让我们看一个简单的例子:

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

  正如我们在上面的例子中所看到的,使用**func关键字后跟函数名来定义 Go 函数。函数所需的参数**需要根据其数据类型定义,最后是返回的数据类型。

  函数的返回也可以在函数中预定义:

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

  这里 c 被定义为返回变量。因此,定义的变量 c 将自动返回,而无需在结尾的 return 语句中定义。

  还可以从单个函数返回多个返回值,将返回值与逗号分隔开。

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

//方式 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

方法

  方法Method是具有接收器的特殊类型的功能*。*接收器既可以是值,也可以是指针。让我们创建一个名为 describe 的方法,它具有我们在上面的例子中创建的接收器类型:

package main
import "fmt"

// struct defination
type person struct {
  name   string
  age    int
  gender string
}

// method 定义
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.describe。请注意,接收器是指针。使用指针,我们传递对值的引用,因此如果我们在方法中进行任何更改,它将反映在接收器pp中。它也不会创建对象的新副本,这样可以节省内存开销。

  请注意,在上面的示例中,age 的值已更改,而 name 的值未更改,因为方法 setName 属于接收器类型,而 setAge 属于指针类型。

接口

  Go 接口interface是方法的集合。接口有助于将类型的属性组合在一起。我们以接口animal为例:

type animal interface {
  description() string
}

  这里的animal是一种接口interface类型。现在我们创建两种不同类型的动物来实现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函数中,我们创建了一个a类型为animal的变量。我们为动物分配蛇和猫类型,并使用 Println 打印a.description()。由于我们以不同的方式实现了两种类型(猫和蛇)中描述的方法,我们得到了不同动物的属性。

  我们在 Go 中编写所有代码。main包是程序执行的入口点。Go 中有很多内置包Package。我们一直使用的就是著名的fmt包。

  在主要机制中使用 Go 的包进行大规模编程,可以将大型项目分成更小的部分。

安装包

go get <package-url-github>
// 示例
go get github.com/satori/go.uuid

  我们安装的软件包保存在GOPATH env中,这是我们的工作目录。通过我们的工作目录中的 pkg 文件夹进入包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

  现在让我们回到 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

  现在我们可以导入person我们创建的包并使用函数Description。请注意,secretName我们在包中创建的功能将无法访问。在 Go 中,以大写字母开头的方法名称将是私有的private

包文档

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

godoc person Description

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

godoc -http=":8080"

  现在转到http://localhost:8080/pkg查看我们刚创建的包的文档。

Go 的内置包

fmt

  该包实现了格式化的 I/O 功能,我们已经使用该包打印出 stdout。

json

  Go 中另一个有用的包是 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 映射到的响应类型struct的地址。请注意,json:"page"映射页面键是结构中的PageNumber键。

错误处理

Error

  错误Error是程序不被希望出现的意外的结果。假设我们正在对外部服务进行 API 调用,此 API 调用可能成功也可能失败。当存在错误类型时,我们就可以识别 Go 程序中的错误。看看下面这个例子:

resp, err := http.Get("http://example.com/")

  这里对错误对象的 API 调用可能会成功或失败。我们可以检查错误是否为nil,并处理响应:

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)
}
自定义错误

  当我们编写自己的函数时,有些情况下我们会遇到错误。可以在错误对象的帮助下返回这些错误:

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)
  }
}

  在 Go 中构建的大多数包或我们使用的外部包都有错误处理机制。所以我们调用的任何函数都可能存在错误。这些错误永远不应该被忽略,并且总是在我们称之为函数的地方优雅地处理,就像我们在上面的例子中所做的那样。

Panic

panic是一种未经处理的事件,在程序执行期间突然遇到。在 Go 中,panic不是处理程序中异常的理想方式。建议使用错误对象。发生panic时程序会停止执行。panic之后执行的事件就是defer

Defer

  defer总是在函数结束时执行。

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)
}

  在上面的例子中,我们使用panic()来故意终止程序的执行。正如你所注意到的,有一个defer语句,它将使程序在程序执行结束时执行该行。当我们需要在函数结束时执行某些操作时,也可以使用defer,例如关闭文件。

并发

  Go 是建立在并发性的基础上的。Go 中的并发可以通过轻量级线程的 Go 例程来实现。

协程 (go routine)

  go 协程routine是可以与另一个函数并行或同时运行的函数。创建 go 协程非常简单。只需在函数前面添加关键字go,我们就可以使它并行执行。go 协程非常轻量级,因此我们可以创建数千个协程。让我们看一个简单的例子:

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 协程,它与主 Go 线程并行执行。有时我们希望在多个线程之间共享资源。Go 更倾向于一个线程的变量不与另一个线程共享,因为这会增加死锁和资源等待的可能性。还有另一种在 Go 协程之间共享资源的方法:管道Channels

管道(channel)

  我们可以使用通道在两个 Go 协程之间传递数据。在创建channel时,必须指定channel接收的数据类型。让我们创建一个字符串类型的简单channel,如下所示:

c := make(chan string)

  使用此channel,我们可以发送字符串类型数据。可以在此频道中发送接收数据:

package main

import "fmt"

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

  接收方等待发送方向channel发送数据。

单向通道

  在某些情况下,我们希望 Go 协程通过channel接收数据但不发送数据,反之亦然。为此,我们还可以创建单向通道。让我们看一个简单的例子:

package main

import (
 "fmt"
)

func main() {
 ch := make(chan string)
 
 go sc(ch)
 fmt.Println(<-ch)
}

func sc(ch chan<- string) {
 ch <- "hello"
}

  在上面的例子中,sc 是一个 Go 协程,它只能向通道发送消息但不能接收消息。

使用 select 为 Go 例程组织多个通道

  函数可能有多个通道正在等待执行。为此,我们可以使用 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"
}

  在上面的示例中,main正在等待两个管道c1c2。使用select case语句打印主函数,消息从管道发送,无论它先收到哪个。

缓冲通道

  有些情况下我们需要向管道发送多个数据。可以为此创建缓冲通道buffered channel。使用缓冲通道,接收器在缓冲区已满之前不会收到消息。我们来看看这个例子:

package main

import "fmt"

func main(){
  ch := make(chan string, 2)
  ch <- "hello"
  ch <- "world"
  fmt.Println(<-ch)
}

结尾

为什么 Golang 会成功?

简单… — Rob-pike

目前为止我们已经了解了 Go 的一些主要组件和功能:

  1. 变量,数据类型
  2. Array、Slices 和 Map
  3. 函数
  4. 循环和条件语句
  5. 指针
  6. 结构、方法和接口
  7. 错误处理
  8. 并发 - Go routine 和 channel

恭喜你,你现在对 Go 有了不错的认识。

抛弃了 1000 行代码的那天是我最富有成效的日子之一。

— Ken Thompson

不要止步于此,继续前进。在大脑中思考一个小规模的应用程序并开始构建。