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

https://learnku.com/go/t/24715

工作区

  • Go 中的工作空间由环境变量「GOPATH」定义。你写的任何代码都将写在工作区内。

  • Go 将搜索 GOPATH 目录中的任何包,或者在安装 Go 时默认设置的 GOROOT 目录。

  • GOROOT 是安装 go 的路径。

所以 , 我们可以设置GOPATH为你想要的目录 , 然后就可以写代码了

1
2
3
4
5
# 写入 env
export GOPATH=~/workspace

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

数据类型

数组和切片

1
2
3
4
5
6
7
8
9
10
11
12
13
// 数组在声明中定义要指定长度,因此不能进行扩展。 数组声明为:
var a [5]int

// 数组也可以是多维的。
var multiD [2][3]int

// 当数组的值在运行时不能进行更改。
// 数组也不提供获取子数组的能力。 为此,Go 有一个名为切片的数据类型。
// 切片声明类似于数组声明 --- 没有定义容量:
var b []int

// 切片也可以定义容量和长度。 我们可以使用以下语法:
numbers := make([]int,5,10)

切片是数组的抽象。 切片使用数组作为底层结构。 切片包含三个组件

  • 容量
  • 长度
  • 指向底层数组的指针

img

1
2
3
4
5
6
7
8
// 通过使用 append 或 copy 函数可以增加切片的容量。
// append 函数可以为数组的末尾增加值,并在需要时增加容量。
numbers = append(numbers, 1, 2, 3, 4)

// 增加切片容量的另一种方法是使用复制功能。
// 只需创建另一个具有更大容量的切片,并将原始切片复制到新创建的切片:
number2 := make([]int, 15) // 创建切片
copy(number2, numbers) // 将原始切片复制到新切片

map

1
2
// 声明一个 map :
m := make(map[string]int)

指针

指针是保存值的地址的地方。

1
2
3
4
5
6
7
8
9
10
 // 一个指针用 * 定义 。根据数据类型定义指针。 
var ap *int

// & 运算符可用于获取变量的地址。
a := 12
ap = &a

// 可以使用 * 运算符访问指针指向的值:
fmt.Println(*ap)
// => 12

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

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

函数

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

1
2
3
4
5
6
7
8
9
10
// add函数没有 return c 语句 , 但是还是顺利返回c变量
func add(a int, b int) (c int) {
c = a + b
return
}

func main() {
fmt.Println(add(2, 1))
}
//=> 3

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

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

1
2
3
4
5
6
7
8
9
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 的属性,其中可能包括姓名,年龄,性别。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 定义
type person struct {
name string
age int
gender string
}

// 创建一个 person 实例 p:
p := person{name: "Bob", age: 42, gender: "Male"} //方式1:指定属性和值
person{"Bob", 42, "Male"} //方式2:指定值

// 可以使用其指针直接访问结构体里面的属性:
pp = &person{name: "Bob", age: 42, gender: "Male"}
pp.name
//=> Bob

方法

  • 方法是一个特殊类型的带有返回值的函数。返回值既可以是值,也可以是指针。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//定义结构体
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
}

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

接口

  • Go 的接口是一系列方法的集合。
  • 接口有助于将类型的属性组合在一起。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 定义接口
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!!!

包的安装

1
2
3
go get <package-url-github>
// 例子
go get [github.com/satori/go.uuid](https://github.com/satori/go.uuid)
  • 我们安装的包保存在环境变量 env 的 GOPATH 目录下,这是我们的工作目录
  • 你可以通过我们的工作目录 cd $GOPATH/pkg 中的 pkg 文件夹查看到下载的包。

创建自定义包

  1. 要创建自定义包,首先我们需要创建一个和包名一样的文件夹。现在在custom_package目录下创建一个person目录

  2. 创建一个 person.go 文件。

    1
    2
    3
    4
    5
    6
    7
    8
    package person
    func Description(name string) string {
    return "The person name is: " + name
    }

    func secretName(name string) string {
    return "Do not share"
    }
  3. 现在需要安装这个包,这样它才可被引入和使用。我们安装一下:

    1
    go install
  4. 创建 main.go 文件。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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 拥有内建的包文档支持功能。运行如下命令生成文档。

1
godoc person Description

它将会为我们的 person 包内部的 Description 函数生成文档。

查看文档的话只需要使用如下命令启动一个 web 服务器就可以:

1
godoc -http=":8080"

错误处理

1
2
3
4
5
6
7
8
9
10
11
12
13
import (
"fmt"
"net/http"
)

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

从函数返回自定义错误

有些情况下存在错误要处理,我们利用 error 对象返回这些错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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)
}
}

Panic

  • panic 是在程序执行期间突然遇到,未经处理的异常
  • 在 Go 中,panic 不是处理程序中异常的理想方式。 建议使用 error 对象。
  • 发生 panic 时,程序执行停止。 panic 之后要继续执行的程序就使用 defer。

Defer

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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)
}

//Calling g.
//Printing in g 0
//Printing in g 1
//Printing in g 2
//Printing in g 3
//Panicking!
//Defer in g 3
//Defer in g 2
//Defer in g 1
//Defer in g 0
//Recovered in f 4
//Returned normally from f.

并发

Go 中的并发可以通过轻量级线程的 Go routine 来实现。

Go routine

  • Go routine 是可以与另一个函数并行或并发的函数。
  • 创建 Go routine 非常简单。 只需在函数前面添加关键字 Go,我们就可以使它并行执行。
  • Go routine 非常简单非常轻量级,因此我们可以创建数千个例程。
1
2
3
4
5
6
7
8
9
10
11
12
func c() {
time.Sleep(time.Second * 2)
fmt.Println("I am concurrent")
}

func main() {
go c()
fmt.Println("I am main")
time.Sleep(time.Second * 2)
}
//=> I am main
//=> I am concurrent
  • 函数 c 是一个 Go routine,它与 Go 程序的主线程并行执行
  • 有时我们希望在多个线程之间共享资源。 Go 不是将一个线程的变量与另一个线程共享,因为这会增加死锁和资源等待的可能性。
  • 还有另一种在 Go routine 之间共享资源的方法:通过 Go 语言的通道。

通道

通道在两个 Go routine 之间传递数据。 在创建通道时,必须指定通道接收的数据类型。

1
2
3
4
5
6
7
8
9
10
11
// 创建一个 string 类型的简单通道
c := make(chan string)

// 有了这个通道,我们可以发送 string 类型数据。 我们都可以在此通道中发送和接收数据:
func main(){
c := make(chan string)
go func(){ c <- "hello" }()
msg := <-c
fmt.Println(msg)
}
//=>"hello"

接收方通道将会一直等待发送方向通道发送数据。

单向通道

希望 Go routine 通过通道接收数据但不发送数据,反之亦然。 为此,我们还可以创建单向通道。

1
2
3
4
5
6
7
8
9
func sc(ch chan<- string) {
ch <- "hello"
}

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

使用 select 为 Go routine 处理多个通道

一个进程里面可能有多个通道正在等待。为此,我们可以使用 select 语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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"
}


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

main 方法正在等待读取 c1 和 c2 通道的数据。 使用 select case 语句打印出结果,消息会通过通道发送过来,会打印出先发送过来的消息。

缓冲通道

使用缓冲通道, 在缓冲区满之前接受方不会收到任何消息。

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