Golang 快速入门(译文) | Go主题月

635 阅读15分钟

image.png 让我们先介绍一下 Go(或 Golang )。Go 是由谷歌工程师 Robert Griesemer 、Rob Pike 和 Ken Thompson 设计的。它是一种静态类型的编译语言。第一个版本于2012年3月作为开源发布。

“Go是一种开源编程语言,它使构建简单、可靠、高效的软件变得容易。”。 — GoLang

在许多语言中,有许多方法可以解决给定的问题。程序员可以花很多时间去思考解决这个问题的最佳方法。

然而,Go 只有一种正确的方法去解决问题。

这节省了开发人员的时间,并使大型代码库易于维护。Go 中没有地图和过滤器这样的“表达性”功能。

“当你有增加表现力的功能时候,通常会同时增加开支”——罗伯·派克

image.png

最近发布的 golang 新 logo:blog.golang.org/go-brand

入门

Go 是由包组成的。包 main 告诉 Go 编译器程序被编译为可执行文件,而不是共享库。它是应用程序的入口点。主包定义为:

package main

让我们在 Go 工作区中创建一个文件 main.go 来编写一个简单的 helloworld 示例。

工作区

Go 中的工作区由环境变量 GOPATH 定义。

您编写的任何代码都将在工作区内编写。Go 将搜索 GOPATH 目录或 GOROOT 目录中的任何包,这在安装 Go 时是默认设置的。GOROOT 是安装 go 的路径。

GOPATH 设置为所需目录。现在,让我们将其添加到文件夹 ~/workspace

# export env
export GOPATH=~/workspace
# go inside the workspace directory
cd ~/workspace

在我们刚刚创建的工作区文件夹中创建文件 main.go ,并编写以下代码。

Hello World!

package main

import (
 "fmt"
)

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

在上面的示例中,fmt 是一个内置的 Go 包,它实现了格式化 I/O 的功能。

我们使用 import 关键字导入一个包。func main 是执行代码的主要入口点。Println 是一个包 fmt 的一个函数,它为我们打印 “helloworld” 。

让我们通过运行这个文件来查看。有两种方法可以运行 Go 命令。正如我们所知,Go 是一种编译语言,因此我们首先需要在执行之前编译它。

> go build main.go

这将创建一个二进制可执行文件 main,现在可以运行:

> ./main
 # Hello World!

还有一种更简单的方法来运行程序。go run 命令有助于抽象编译步骤。您只需运行以下命令即可执行该程序。

go run main.go
# Hello World!

注意:要试用本博客中提到的代码,您可以使用 play.golang.org

变量

Go 中的变量是显式声明的。Go 是一种静态类型的语言。这意味着在声明变量时检查变量类型。变量可以声明为:

var a int

在这种情况下,该值将设置为0。使用以下语法声明并初始化具有不同值的变量:

var a = 1

在这里,变量被自动赋值为 int。我们可以使用变量声明的简写定义:

message := "hello world"

我们也可以在同一行中声明多个变量:

var b, c int = 2, 3

数据类型

与任何其他编程语言一样,Go 支持各种不同的数据结构。让我们来探索其中一些:

Number,String,Boolean

支持的 Number 存储类型有 int、int8、int16、int32、int64、uint、uint8、uint16、uint32、uint64、uintptr…

String 类型存储字节序列。它用关键字 string 表示和声明。

Boolean 使用关键字 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)

Array,Slice,Maps

Arrays(数组) 是具有相同数据类型的元素序列。数组在声明处定义了一个固定长度,因此不能扩展得超过这个长度。一个数组的声明为:

var a [5]int

数组也可以是多维的。我们可以简单地用以下格式创建它们:

var multiD [2][3]int

数组限制了在运行时数组值发生变化时的情况。数组也不提供获得子数组的能力。为此,Go 有一个名为 Slices 的数据类型。

Slices(切片) 存储一系列元素,可以随时展开。切片声明类似于数组声明,但没有定义容量:

var b []int

这将创建一个容量为0、长度为0的切片 。切片也可以定义容量和长度来。我们可以使用以下语法:

numbers := make([]int,5,10)

这里,切片的初始长度为 5,容量为 10。

切片是对数组的抽象。切片使用数组作为底层结构。切片包含三个组件:容量、长度和指向底层数组的指针,如下图所示:

image.png

图片来源: blog.golang.org/go-slices-u…

切片的容量可以通过使用 append 或 copy 函数来增加。append 函数为数组的末尾添加值,并在需要时增加容量。

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

增加切片容量的另一种方法是使用 copy 函数。只需创建另一个容量更大的切片,并将原始切片复制到新创建的切片:

// create a new slice
number2 := make([]int, 15)
// copy the original slice to new slice
copy(number2, number)

我们可以创建一个切片的子切片。只需使用以下命令即可完成此操作:

// initialize a slice with 4 len and values
number2 = []int{1,2,3,4}
fmt.Println(numbers) // -> [1 2 3 4]
// create sub slices
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]

Maps 是 Go 中的一种数据类型,它将键映射到值。我们可以使用以下命令定义 map :

var m map[string]int

这里 m 是一个新的 map 变量,它的键是 string ,值是 integers 。我们可以轻松地向 map 添加键值对:

// adding key/value
m['clearity'] = 2
m['simplicity'] = 3
// printing the values
fmt.Println(m['clearity']) // -> 2
fmt.Println(m['simplicity']) // -> 3

类型转换

可以使用类型转换将一种类型的数据类型转换为另一种类型。让我们看看一个简单的类型转换:

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 有助于组织多个条件语句。以下示例显示了一个简单的 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 循环。相同的 for 语句可用于普通 for 循环:

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 中,并在需要时导入 fmt 或其他包,如第一部分所示 main.go 上面的例子。

函数

main 包中定义的 func 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 被定义为返回变量。因此,定义的变量 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)
}

Struct,Method,Interface

Go 不是一种完全面向对象的语言,但是它有结构、接口和方法,它有很多面向对象的支持和感觉。

Struct

Struct(结构体) 是不同字段的类型化集合。结构体用于将数据分组在一起。例如,如果我们想对一个人类型的数据进行分组,我们可以定义一个人的属性,该属性可以包括姓名、年龄、性别。可以使用以下语法定义结构:

type person struct {
  name string
  age int
  gender string
}

定义了 person 类型结构之后,现在让我们创建一个 person :

//way 1: specifying attribute and value
p = person{name: "Bob", age: 42, gender: "Male"}
//way 2: specifying only value
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

Method(方法) 是一种具有返回值的特殊类型的函数。返回值既可以是值,也可以是指针。让我们创建一个名为 descripe 的方法,该方法具有我们在上述示例中创建的接收者类型 person :

package main
import "fmt"

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

// method defination
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.descripe ,请注意,返回值是指针类型。通过指针,我们传递一个对值的引用,因此如果我们对方法进行任何更改,它将反映在返回值 pp 中。它也不会创建对象的新副本,从而节省内存。

注意,在上面的示例中,age 的值被更改,而 name 的值没有更改,因为方法 setName 是 值类型,而 setAge 是 类型指针。

Interface

Interface(接口)是方法的集合。接口帮助将类型的属性组合在一起。以接口 animal 为例:

type animal interface {
  description() string
}

这里是一个接口类型。现在让我们创建两个实例来实现 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。我们将 snakecat 类型指定给 animal ,并使用 Println 打印描述。由于我们以不同的方式实现了这两种类型( snakecat )中的描述方法,所以我们得到了 animal 的描述。

我们把所有的代码都写在一个包里。main 包是程序执行的入口点。Go 中有很多内置包。我们使用的最著名的是 fmt 包。

“Go提供了一个大型编程的主要机制中的 Go 包,它们使得把一个大型项目划分成更小的部分成为可能。” -罗伯特·格里默

安装 包

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

我们安装的包保存在环境变量 env 中,环境变量 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 并使用函数描述。

请注意,我们在包中创建的函数 secretName 将不可访问。在 Go 中,以不带大写字母开头的方法名将是私有的。

包文档

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

godoc person Description

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

godoc -http=":8080"

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

Go中的一些内置包

fmt

该包实现格式化的 I/O 函数。我们已经使用了打印到标准输出的软件包。

json

Go 中另一个有用的包是 json 包。这有助于对 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))
}
json 解码:
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

在使用 unmarsha l对 json字 节进行解码时,第一个参数是 json 字节,第二个参数是我们希望 json 映射到的响应类型 struct 的地址。请注意,json:“page” 将结构中的 page 键映射到 PageNumber 键。

错误处理

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

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

在这里,对 error 对象的 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)
}

从函数返回自定义错误

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

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 之后被执行的是延迟。

//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 总是在函数结束时执行。

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

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

并发

Go 的构建考虑了并发性。Go 中的并发可以通过轻量级线程的 Go routine 来实现。

Go routine

Go routine 是可以与另一个函数并行或并发运行的函数。创建一个 Go routine 非常简单。只需在函数前面添加一个关键字 Go,我们就可以使它并行执行。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 ,它与 Go 程序的主线程并行执行。有时我们希望在多个线程之间共享资源。Go 更喜欢不与另一个线程共享一个线程的变量,因为这增加了死锁和资源等待的机会。还有一种方法可以在 Go routine 之间共享资源:通过Go 的 Channel

Channel

我们可以使用 Channel(通道) 在两个 Go routine 之间传递数据。在创建通道时,需要指定通道接收的数据类型。让我们创建一个字符串类型的简单通道,如下所示:

c := make(chan string)

通过这个通道,我们可以发送字符串类型的数据。我们可以在此通道中发送和接收数据:

package main

import "fmt"

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

接收方通道等待发送方将数据发送到通道。

单向通道

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

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 routine ,它只能向通道发送消息,但不能接收消息。

使用 select 为 Go routine 组织多个通道

函数可能正在等待多个通道。为此,我们可以使用 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 语句,func main 打印消息,消息从通道发送,以最先接收的为准。

缓冲信道

您可以在 go 中创建缓冲通道。对于缓冲通道,如果缓冲区已满,则发送到通道的消息将被阻止。我们来看看这个例子:

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!

正如我们在上面所看到的,一个通道接受的消息不超过2条。

Golang 为什么成功

Simplicity… — Rob-pike

太好啦~~~

我们学习了Go的一些主要组件和特性。

  • 变量,数据类型

  • 阵列切片和贴图

  • 函数

  • 循环语句和条件语句

  • 指针

  • 结构、方法和接口

  • 错误处理

  • 并发 - Go routine 和通道

恭喜,你现在对 Go 有了相当的了解。

我最富有成效的一天就是扔掉了1000行代码。 -肯汤普森

不要停在这里。继续前进。考虑一个小应用程序并开始构建。

LinkedInGithubTwitter

也发表在 Milap Neupane 博客上:Learning Go-from zero to hero

原文链接:www.freecodecamp.org/news/learni…