Golang 新手教程:入门速成指南

file

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

“Go 是一种开源编程语言,可以轻松构建的简单,可靠,高效的软件”。

— 关于 go

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

然而,Go却是只有一种正确的方法来解决问题的语言。

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

“如果你有增加表现力的功能,通常会增加费用”

— Rob Pike

file

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

入门

Go 是由包组成的。 main 包告诉 Go 编译器该程序可以被编译成可执行文件,而不是一个共享的库。它是应用程序的入口。main 包被定义为如下格式:

package main

接下来,让我们通过在 Go 工作区中创建一个文件 main.go 来编写一个简单的 hello world 示例。

go 的工作区

Go 中的工作空间由环境变量「GOPATH」定义。你写的任何代码都将写在工作区内。Go 将搜索 GOPATH 目录中的任何包,或者在安装 Go 时默认设置的GOROOT 目录。 GOROOT 是安装go的路径。

GOPATH 设置为你想要的目录。 现在,让我们将它添加到文件夹〜/ workspace 中。

# 写入 env
export GOPATH=~/workspace

# cd 到工作区目录\
cd ~/workspace

使用我们刚刚创建的工作空间文件夹中的以下代码创建文件 main.go

Hello World!

package main

import (
 "fmt"
)

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

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

在 Go 中我们导入一个包使用 import 关键字。func main 是代码执行的入口。Printlnfmt 包中的一个函数,它为我们打印 “hello world”。

让我们看一下运行这个文件。 我们可以通过两种方式运行 Go 命令。 我们知道,Go 是一种编译语言,所以我们首先需要在执行之前编译它。

> go build main.go

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

> ./main
 # Hello World!

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

go run main.go
 # Hello World!

Note: 您可以使用 https://play.golang.org 来运行本文提到的代码。

变量

变量在 Go 语言中是一个很明确的定义。 Go 是一种静态类型的语言。这意味着在声明变量时我们就需要明确变量的类型。一般一个变量的定义如下:

var a int

上面的实例中,我们定义了一个 int 类型的变量 a ,默认会被赋值成 0 。使用以下语法可以初始化改变变量的值:

var a = 1

这里我们没有制定变量 a 的类型,在我们给它初始化为1时,它就自动被定义成了 int 类型的变量。我们也可以使用一种更简短的语法来定义它:

message := "hello world"

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

var b, c int = 2, 3

数据类型

跟任何其他编程语言一样,Go 语言支持各种不同的数据结构。 让我们探讨一下:

Number, String, and 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)

数组, 切片, 以及 Maps

数组是相同数据类型的元素序列。 数组在声明中定义要指定长度,因此不能进行扩展。 数组声明为:

var a [5]int

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

var multiD [2][3]int

当数组的值在运行时不能进行更改。 数组也不提供获取子数组的能力。 为此,Go 有一个名为切片的数据类型。

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

var b []int

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

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

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

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

file

图片地址: blog.golang.org/go-slices-usage-an...

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

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

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

// 创建切片
number2 := make([]int, 15)
// 将原始切片复制到新切片
copy(number2, numbers)

我们可以创建切片的子切片。 这可以使用以下命令完成:

// 初始化长度为 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 的一种 Key-Value类型的数据结构,我们可以通过下面的命令声明一个 map :

m := make(map[string]int)

m 是 一个 Key 类型为 string、Value 类型为 int的 map 类型的变量。我们可以很容易地添加键值对到 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 cases 有助于组织多个条件语句。 以下示例显示了一个简单的 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

在将结构体作为参数传递或者为已定义类型声明方法时,通常首选指针。

  1. 传递值时,实际复制的值意味着更多的内存
  2. 传递指针后,函数更改的值将反映在方法/函数调用者中。

例子:

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

Note: 当你在博客中尝试示例代码时,不要忘记将其包含在 main 包中,并在需要时导入 fmt 或其他包,如上面第一个 main.go 示例所示。

函数

main 函数 定义在 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 不是绝对的面向对象的语言, 但是使用结构体,接口和方法,它有很多面向对象的风格以及对面向对象的支持。

结构体

结构体是不同字段的类型集合。 结构用于将数据分组在一起。 例如,如果我们想要对 Person 类型的数据进行分组,我们会定义一个 person 的属性,其中可能包括姓名,年龄,性别。 可以使用以下语法定义结构:


type person struct {
  name string
  age int
  gender string
}

在定义了 person 结构体的情况下,现在让我们创建一个 person 实例 p:

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

方法

方法是一个特殊类型的带有返回值的函数。返回值既可以是值,也可以是指针。让我们创建一个名为 describe 的方法,它具有我们在上面的例子中创建的 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.describe 这样。请注意,返回值是指针类型。使用指针,我们传递对值的引用,因此如果我们对方法进行任何更改,它将反映在返回值 pp 中。指针类型的返回值也不会创建对象的新副本,从而节省了内存。

请注意,在上面的示例中,age 的值已更改,而 name 的值不会改变。因为方法 setName 是返回值是值类型,而 setAge 方法的返回值是类型指针。

接口

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

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。我们为 animal 接口指定了 snake 和 cat 两个实例对象,并使用 Println 方法打印 a.description 。

我们所有用 go 语言写的代码都是在包含在对应的包中。 main 包是程序执行的入口。Go中有很多内置包。 我们使用的一个最常见的包是 fmt 包

「Go 的包主要是用来进行大规模编程,并且可以将大型项目分成更小的部分。」

— Robert Griesemer

包的安装

go get <package-url-github>
// 例子
go get [github.com/satori/go.uuid](https://github.com/satori/go.uuid)

我们安装的包保存在环境变量 env 的 GOPATH 目录下,这是我们的工作目录。 你可以通过我们的工作目录 cd $GOPATH/pkg 中的 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 语言中,方法名称为非大写字母开头的,即为私有方法。

包文档

Go 拥有内建的包文档支持功能。运行如下命令生成文档。

godoc person Description

它将会为我们的 person 包内部的 Description 函数生成文档。查看文档的话只需要使用如下命令启动一个 web 服务器就可以:

godoc -http=":8080"

现在去访问这个 URL http://localhost:8080/pkg/ 然后你就可以看到我们刚创建的包文档了。

Go 中部分常见的内置包

fmt

fmt 包实现了格式化 I/O 的功能。我们可以使用这个包来打印到标准输出。

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" 将 page 的键映射到结构体中的 PageNumber 的键。

错误处理

错误是程序不希望出现的意外结果。假设我们正在对一个外部服务的 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)
}

从函数返回自定义错误

当我们写一个自己的函数时, 在有些情况下存在错误要处理,我们利用 error 对象返回这些错误:

func Increment(n int) (int, error) {
  if n < 0 {
    // 返回一个 error 对象
    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 不是处理程序中异常的理想方式。 建议使用 error 对象。 发生 panic 时,程序执行停止。 panic 之后要继续执行的程序就使用 defer。

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,例如关闭文件。

并发

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

Go routine

Go routine 是可以与另一个函数并行或并发的函数。 创建 Go routine非常简单。 只需在函数前面添加关键字Go,我们就可以使它并行执行。 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 语言的通道。

通道

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

c := make(chan string)

有了这个通道,我们可以发送 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 方法正在等待读取 c1 和 c2 通道的数据。 使用 select case 语句打印出结果,消息会通过通道发送过来,会打印出先发送过来的消息。

缓冲通道

有些情况下,我们需要向一个通道发送多个数据。 你可以为此创建一个缓冲通道。使用缓冲通道, 在缓冲区满之前接受方不会收到任何消息。 让我们看一下这个例子:

package main

import "fmt"

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

为什么Golang成功?

Simplicity… —  Rob-pike

很好!

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

  1. 变量,数据类型
  2. 数组 切片和映射
  3. 函数
  4. 流程控制语句
  5. 指针
  6. 方法,结构体和接口
  7. 错误处理
  8. 并发— Go 的线程和通道

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

我效率最高的一天是抛弃 1000 行代码。

— Ken Thompson

不要止步于此。继续前进。 考虑一个小的应用程序并开始构建它吧。

本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://medium.freecodecamp.org/learning...

译文地址:https://learnku.com/go/t/24715

本帖已被设为精华帖!
本文为协同翻译文章,如您发现瑕疵请点击「改进」按钮提交优化建议
讨论数量: 16
guanhui07

nice!!!

5年前 评论

非常好的教程,加油!

5年前 评论
  str := `{"page": 1, "fruits": ["apple", "peach"]}`

这是个什么语法

5年前 评论
AKA-TanNaWen 4年前

@lovecn

  • 就是实例化了一个 json 字符串,fruits 对应的是一个数组。
  • 在Go 语言中,字符串字面量可以使用双引号 "" 或者反引号 ' 来创建
5年前 评论

感谢分享。
目前基本语法认识的差不多了,但是感觉不知道该如何进阶。能大概看懂别人的代码,可如果要自己来写,却不知道如何下笔。尴尬

4年前 评论
playmaker

nice !!! 就是有错别字 panc => panic ,像 => 向。 :see_no_evil:

3年前 评论

nice,教程很棒~

2年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!