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
main.go

go 的工作区

GOPATHGOROOTGOROOT
GOPATH〜/ workspace
# 写入 env
export GOPATH=~/workspace

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

Hello World!

package main

import (
 "fmt"
)

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

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

> go build main.go
main
> ./main
 # Hello World!
go run
go run main.go
 # Hello World!

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

变量

GoGo
var a int
inta
var a = 1
aint
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
bool
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

appendcopyappend
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)
mstringint
// 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

请注意,在上面的示例中,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!!!
a

我们所有用 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)
cd $GOPATH/pkg

创建自定义包

我们从创建 custom_package 文件夹开始:

> mkdir custom_package
> cd custom_package
personcustom_packageperson
> 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
personsecretName

包文档

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
json:"page"

错误处理

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

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