前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >GO语言入门教程

GO语言入门教程

作者头像
终身幼稚园
发布2020-05-18 17:49:35
9270
发布2020-05-18 17:49:35
举报
文章被收录于专栏:终身幼稚园终身幼稚园

本文翻译于Milap Neupane Blog的Learning Golang — from zero to hero

前言

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

Go 语言被设计成一门应用于搭载 Web 服务器,存储集群或类似用途的巨型中央服务器的系统编程语言。

对于高性能分布式系统领域而言,Go 语言无疑比大多数其它语言有着更高的开发效率。它提供了海量并行的支持,这对于游戏服务端的开发而言是再好不过了。

开始

Go应用程序是由一个个package组成,其中每个Go应用程序都包含一个名为 main 的包。package main表示一个可独立执行的程序,它是应用程序的入口点。

代码语言:javascript
复制
package main

接下来我们通过创建一个文件main来编写一个简单的hello world示例。进入Go语言的 Workspace。

Workspace 工作空间

Go中的Workspace由环境变量 GOPATH 定义

我们Go代码需要在Workspace中编写。Go将搜索GOPATH目录或GOROOT目录中的任何包,这是安装Go时默认设置的。 其中GOROOT是安装go的路径。

现在我们将GOPATH设置为~/workspace

代码语言:javascript
复制
# export env
export GOPATH=~/workspace
# go inside the workspace directory
cd ~/workspace

在~/workspace目录下创建 main.go 文件

hello world

代码语言:javascript
复制
 package main
 import (
     "fmt"
 )
 func main() {
     fmt.Println("Hello World!")
 }

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

我们使用 import 在Go中导入一个包。func main 是执行代码的主要入口点。Println是包fmt中的一个函数,它为我们打印“hello world”。

让我们运行这个文件。运行Go命令有两种方法。因为Go是一种编译语言,所以我们首先需要在执行之前编译它。

代码语言:javascript
复制
> go build main.go

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

代码语言:javascript
复制
> ./main
# Hello World!

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

代码语言:javascript
复制
> go run main.go
# Hello World!

变量

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

代码语言:javascript
复制
var a int 

在本例中,值将设置为0。使用以下语法声明和初始化具有不同值的变量:

代码语言:javascript
复制
var a = 1

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

代码语言:javascript
复制
message := "hello world"

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

代码语言:javascript
复制
var b, c int = 2, 3

数据类型

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

数字型,布尔型, 字符串型

数字类型: int, int8, int16, int32, int64,uint, uint8, uint16, uint32, uint64, uintptr…

布尔型: bool

字符串型: 字符串就是一串固定长度的字符连接起来的字符序列。关键字 string

Go语言还支持复数类型的数据类型,可以用complex64和complex128来声明

代码语言:javascript
复制
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)

数组, 切片(Slice), Map

数组是相同数据类型的元素序列。数组在声明时定义了一个固定的长度,因此不能进行更大的扩展。数组声明为:

代码语言:javascript
复制
var a [5]int

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

代码语言:javascript
复制
var multiD [2][3]int

Go 数组的长度不可改变,在特定场景中这样的集合就不太适用,Go中提供了一种灵活,功能强悍的内置类型切片("动态数组"),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。

切片存储元素序列,可以在任何时候展开。切片声明类似于数组声明,不需要定义容量:

代码语言:javascript
复制
var b []int

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

代码语言:javascript
复制
numbers := make([]int,5,10)

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

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

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

代码语言:javascript
复制
numbers = append(numbers, 1, 2, 3, 4)

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

代码语言:javascript
复制
// create a new slice
number2 := make([]int, 15)
// copy the original slice to new slice
copy(number2, number)

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

代码语言:javascript
复制
package main

 import (
     "fmt"
 )

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

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

代码语言:javascript
复制
var m map[string]int

这里m是新的map变量,它的键是字符串,值是整数。我们可以很容易地添加键和值到map:

代码语言:javascript
复制
package main

 import (
     "fmt"
 )

 func main() {
     m := make(map[string]int)
     // adding key/value
     m["clearity"] = 2
     m["simplicity"] = 3
     // printing the values
     fmt.Println(m["clearity"])   // -> 2
     fmt.Println(m["simplicity"]) // -> 3
 }

类型转换

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

代码语言:javascript
复制
package main

 import (
     "fmt"
 )

 func increment(i *int) {
     *i++
 }

 func main() {
     a := 1.1
     b := int(a)
     fmt.Println(b)
     //-> 1
 }

并不是所有类型的数据类型都可以转换成另一种类型。确保数据类型与转换兼容。

条件语句

if else

对于条件语句,我们可以使用if-else语句,如下面的示例所示。记住确保花括号与条件位于同一行。

代码语言:javascript
复制
package main

 import (
     "fmt"
 )

 func increment(i *int) {
     *i++
 }

 func main() {
     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语句:

代码语言:javascript
复制
package main

 import (
     "fmt"
 )

 func increment(i *int) {
     *i++
 }

 func main() {
     i := 2
     switch i {
     case 1:
         fmt.Println("one")
     case 2:
         fmt.Println("two")
     default:
         fmt.Println("none")
     }
 }

循环语句

Go语言循环只有一个关键字。一个for循环命令帮助实现不同类型的循环:

代码语言:javascript
复制
package main

 import (
     "fmt"
 )

 func increment(i *int) {
     *i++
 }

 func main() {
     i := 0
     sum := 0
     for i < 10 {
         sum += 1
         i++
     }
     fmt.Println(sum)
 }

面的例子类似于c语言中的while循环。for语句也可以用于普通的for循环:

代码语言:javascript
复制
package main

 import (
     "fmt"
 )

 func increment(i *int) {
     *i++
 }

 func main() {
     sum := 0
     for i := 0; i < 10; i++ {
         sum += i
     }
     fmt.Println(sum)
 }

Go中的无限循环:

代码语言:javascript
复制
for {
}

指针

Go语言提供指针。指针是保存值地址的位置。指针由*定义。指针是根据数据类型定义的。例子:

代码语言:javascript
复制
var ap *int

这里ap是指向整数类型的指针。&运算符可用于获取变量的地址。

代码语言:javascript
复制
a := 12
ap = &a

指针所指向的值可以使用*操作符访问:

代码语言:javascript
复制
fmt.Println(*ap)
// => 12

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

  1. 当传递值时,值实际上被复制,这意味着应用程序将占用更多的内存
  2. 指针传递之后,函数所更改的值将反映回方法/函数调用者中。

例子:

代码语言:javascript
复制
package main

 import (
     "fmt"
 )

 func increment(i *int) {
     *i++
 }

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

注意:当您在尝试示例代码时,不要忘记将其包含在package main中,并在需要时导入fmt或其他包。

函数

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

代码语言:javascript
复制
package main

 import (
     "fmt"
 )

 func add(a int, b int) int {
     c := a + b
     return c
 }

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

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

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

代码语言:javascript
复制
package main

 import (
     "fmt"
 )

 func add(a int, b int) (c int) {
     c = a + b
     return
 }

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

这里c被定义为返回变量。因此,定义的变量c将自动返回,而不需要在最后的return语句中定义。

您还可以从一个用逗号分隔返回值的函数返回多个返回值。

代码语言:javascript
复制
package main

 import (
     "fmt"
 )

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

 //=> successfully added
 //=> 3

方法,结构体和接口

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

结构体struct

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

代码语言:javascript
复制
type person struct {
     name   string
     age    int
     gender string
 }

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

代码语言:javascript
复制
//way 1: specifying attribute and value
p = person{name: "Bob", age: 42, gender: "Male"}
//way 2: specifying only value
person{"Bob", 42, "Male"}

我们可以用.(点)访问这些数据。

代码语言:javascript
复制
p.name
//=> Bob
p.age
//=> 42
p.gender
//=> Male

你也可以直接用它的指针访问结构的属性:

代码语言:javascript
复制
pp = &person{name: "Bob", age: 42, gender: "Male"}
pp.name
//=> Bob

方法

方法是带有接收者的特殊函数类型。接收者可以是值也可以是指针。让我们创建一个名为describe的方法,它具有我们在上面例子中创建的接收者类型person:

代码语言:javascript
复制
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.describe来调用该方法。注意,接收者是一个指针。使用指针,我们将传递一个对该值的引用,所以如果我们对方法做任何更改,它将反映在接受者pp中。它也不创建对象的新副本,这节省了内存。

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

接口

Go语言接口是一组方法。接口帮助将类型的属性组合在一起。让我们以接口animal为例:

代码语言:javascript
复制
type animal interface {
     description() string
 }

这里animal是一个接口类型。现在让我们创建两种不同类型的动物,它们实现了animal接口类型:

代码语言:javascript
复制
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!!!

在主函数中,我们创建了一个animal类型的变量a。我们给这种动物分配了一个snake和一个cat的类型,并使用Println打印a.description。因为我们以不同的方式实现了这两种类型(snake和cat)中的描述方法,所以我们打印了animal的描述。

包package

我们在一个包中用Go语言编写所有代码。main package 是程序执行的入口点。Go中有很多内置包。我们使用的最常用的就属fmt包。

安装包

代码语言:javascript
复制
> go get
// example
> go get github.com/satori/go.uuid

我们安装的包保存在GOPATH 路径下,这是我们的工作目录。通过进入工作目录cd $GOPATH/pkg中的pkg文件夹,您可以看到这些包。

创建自定义包

先从创建文件夹custom_package:

代码语言:javascript
复制
> mkdir custom_package
> cd custom_package

要创建自定义包,我们需要首先创建一个包含所需包名称的文件夹。假设我们正在构建一个package person。为此,让我们在custom_package文件夹中创建一个名为person的文件夹:

代码语言:javascript
复制
> mkdir person
> cd person

现在我们在这个文件夹下创建文件person.go

代码语言:javascript
复制
package person

 func Description(name string) string {
     return "The person name is: " + name
 }

 func secretName(name string) string {
     return "Do not share"
 }

现在我们需要安装这个包,以便导入和使用它。所以让我们安装它:

代码语言:javascript
复制
> go install

现在让我们回到custom_package文件夹并创建一个main.go文件

代码语言:javascript
复制
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语言内置了对包文档的支持。运行以下命令生成文档:

代码语言:javascript
复制
> godoc person Description

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

代码语言:javascript
复制
godoc -http=":8080"

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

Go语言的内置包

fmt

该包实现格式化的I/O函数。

json

Go语言中另一个有用的包是json包。这有助于对JSON进行编码/解码。让我们举一个例子来编码/解码一些json:

编码

代码语言:javascript
复制
package main

 import (
     "encoding/json"
     "fmt"
 )

 func main() {
     mapA := map[string]int{"apple": 5, "lettuce": 7}
     mapB, _ := json.Marshal(mapA)
     fmt.Println(string(mapB))
 }

解码

代码语言:javascript
复制
package main

 import (
     "encoding/json"
     "fmt"
 )

 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”将map键映射到结构中的PageNumber键。

错误处理

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

代码语言:javascript
复制
resp, err := http.Get("http://example.com/")

在这里,对错误对象的API调用可能通过,也可能失败。我们可以检查错误是nil还是present,并相应地处理响应:

代码语言:javascript
复制
 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对象的帮助下返回:

代码语言:javascript
复制
 package main

 import (
     "errors"
     "fmt"
 )

 func Increment(n int) (int, error) {
     if n < 0 {
         // return error object
         return 0, 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)
     }
 }

 // => The person name is: Milap

我们使用的大多数内置或外部包都有错误处理机制。所以我们调用的任何函数都可能有错误。不应该忽略这些错误,并且总是在调用这些函数的地方优雅地处理它们,就像我们在上面的示例中所做的那样。

panic

panic是用来表示非常严重的不可恢复的错误的。在Go中,panic不是处理程序中错误的理想方法。建议使用error对象。当出现panic时,程序执行将停止。panic之后执行的是defer

代码语言:javascript
复制
 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总是在函数末尾执行。

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

并发

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

goroutine

goroutine是可以与其他函数并行或并发运行的函数。创建goroutine非常简单。只需在函数前面添加一个关键字go,我们就可以使它并行执行。goroutine是非常轻量级的,所以我们可以创建数以千计的goroutine。让我们来看一个简单的例子:

代码语言:javascript
复制
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是一个goroutine,它与主Go线程并行执行。有时我们希望在多个线程之间共享资源。Go语言不喜欢将一个线程的变量与另一个线程共享,因为这会增加死锁和资源等待的概率。在goroutine之间共享资源还有另一种方法:通过 通道(channel)。

通道(channel)

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

代码语言:javascript
复制
c := make(chan string)

使用这个通道(channel),我们可以发送字符串类型的数据。我们可以在这个通道(channel)发送和接收数据:

代码语言:javascript
复制
package main

 import "fmt"

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

 //=>"hello"

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

单向通道

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

代码语言:javascript
复制
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语句。让我们看一个例子,以便更清楚:

代码语言:javascript
复制
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 语句,主函数打印消息,消息首先从接收到的通道发送。

缓冲通道

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

代码语言:javascript
复制
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!

总结

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

  • 变量,数据类型
  • 数组,切片(Slice)和map
  • 函数
  • 循环语句和条件语句
  • 指针
  • 方法,结构体和接口
  • 错误处理
  • 并发

相信您已经对Go语言有了大致的了解。


喜欢可以关注wx公众号:终身幼稚园

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-07-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 终身幼稚园 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 开始
    • Workspace 工作空间
      • hello world
      • 变量
      • 数据类型
        • 数字型,布尔型, 字符串型
          • 数组, 切片(Slice), Map
          • 类型转换
          • 条件语句
            • if else
              • switch case
              • 循环语句
              • 指针
              • 函数
              • 方法,结构体和接口
                • 结构体struct
                  • 方法
                    • 接口
                    • 包package
                      • 安装包
                        • 创建自定义包
                          • 包的文档
                            • Go语言的内置包
                              • fmt
                              • json
                          • 错误处理
                            • 从函数返回自定义错误
                            • panic
                            • defer
                            • 并发
                              • goroutine
                                • 通道(channel)
                                  • 单向通道
                                    • 使用select为个go routine组织多个通道
                                      • 缓冲通道
                                      • 总结
                                      领券
                                      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档