查看原文
其他

也许是最简洁版本,一篇文章上手Go语言

Milap Neupane 高可用架构 2019-11-28

导读:Go语言是近年来最热门的编程语言,是Google开发的一种静态强类型、编译型、并发型,并具有垃圾回收功能的编程语言。Go语言同时具备开发效率高和执行效率高两大特点,被誉为云计算时代的C语言。本文作者通过一篇文章带你学会Go语言。


Go 语言是一门开源语言,能够轻松的构建简单,可靠,高效的软件。—— Golang


在很多语言中,解决给定的问题通常有多种方式。工程师需要花费大量的时间思考什么才是解决问题的最优解法。而在Golang中,问题的解法通常只有一种。

这一特性大大节约了工程师的时间,而且使得维护大型代码库变得更容易。在Golang中没有maps和filter这样"高消耗"的特性。

语言的特性带来更好的表现力也带来代价。

——Rob Pike

1. 开始动手

Golang由包组成。Golang编译器将main包编译为可执行文件,而非共享库。main包是应用的入口,通常被定义如下:

package main

下面看一个hello world 的例子,在Golang 的工作空间创建main.go文件。


1.1 工作空间

在Go语言中,工作空间由环境变量GOPATH定义。所有编写的代码需要在工作空间中。Go语言会在GOPATH和GOROOT的路径中搜索包。GoROOT是在安装的时候确定的安装路径。

下面来设置GOPATH,我们将~/workspace 加入工作空间。

# export envexport GOPATH=~/workspace# go inside the workspacecd ~/workspace

1.2 HELLO WORLD!

我们在刚才的工作空间下创建main.go 文件,代码如下:

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

上面的例子中,fmt是Go内置的格式化I/O函数。

我们在Go语言中使用import关键字导入包,func main 是入口函数。Println是fmt包中函数,用于打印 "Hello World!"。

让我们开始运行该文件。众所周知Go是编译型语言,我们在运行之前先进行编译。

> go build main.go

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

> ./main# Hello World!

另一种简单的方式是使用go run 命令:

go run main.go# Hello World!

2. 变量

Go中的变量类型是显式指定的。Go语言是强类型语言,这意味着在变量声明的时候会检查变量类型。

变量定义如下所示:

var a int

在这个例子中,a的初始值被设置为0。用下面的方式可以定义并初始化变量。

var a = 1

这里的变量被编译器推断为int。更简单的变量定义如下所示:

message := "hello world"

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

var b, c int = 2, 3

3.数据类型

3.1 NUMBER,STRING, BOOLEAN

int 的类型有 int, int8, int16, int32, int64, unit, unit8, unit16, unit 32, unit64, unitptr...

String类型使用byte序列存储数据,用关键字string来声明变量。

bool 关键字表示布尔类型。

Golang 也支持复数,用conplex64和complex128表示。

var a bool = truevat b int = 1var c string = "hello world"var d float32 = 1.222var x complex128 = cmplx.Sqrt(-5 +12i)

3.2 ARRAYS,SLICES,MAPS

Array 是同类型元素的数组。Array在声明的时候会指定长度且不能改变。一个数组的定义如下:

var a[5] int

也有多维数组,定义如下

var multiD [2][3]int

Slices 是能随时扩容的同类型元素的序列 。Slice的声明方式如下:

var b []int

这将会创建一个容量为0,长度为0的Slice。Slice也可以定义容量和长度,格式如下:

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

这个Slice初始长度为5,容量为10。

Slice是数组的封装,其内部实现是数组,slice有三个元素,容量,长度和指向内部数组的指针。


Slice的容量可以通过append 或者 copy函数增加。Append函数也能在数组的末尾添加元素,在容量不足的情况下会对slice扩容。

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

另一种增加slice容量的方式是使用copy函数。Copy函数的原理是创建一个新的大容量的slice,并把原有的slice拷贝到新的slice中。

// 创建新的slicenumber2 := make([]int, 15)// 复制原有的slice到新的slicecopy(number2, number)

我们也可以创建slice的子slice。例子如下:

package main
import ( "fmt" )
func main() { // 初始化slice number2 := []int{1, 2, 3, 4} fmt.Println(number2) // -> [1 2 3 4] // 创建子slice 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] }

Go语言中的Map是键值对,定义如下:

var m map[string]int

m是定义的变量名,键的类型是string,值的类型是integers。Map中添加键值对的例子如下:

package main
import ( "fmt" )
func main() { m := make(map[string]int) // 添加键值对 m["clearity"] = 2 m["simplicity"] = 3 // 打印值 fmt.Println(m["clearity"]) // -> 2 fmt.Println(m["simplicity"]) // -> 3 }

4. 类型转换

使用类型转换能够改变数据类型,例子如下:

package main
import ( "fmt" )
func increment(i *int) { *i++ }
func main() { a := 1.1 b := int(a) fmt.Println(b) //-> 1 }

5. 条件表达式

5.1 IF ELSE

If else 的例子如下,需要注意的是花括号和条件表达式位于同一行。

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

5.2 SWITCH CASE

Switch case 能组织多条件表达式,例子如下:

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

6. 循环

Golang中只有一个循环表达的关键字,不同形式的循环表达式如下:

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循环类似,更为正式的循环表达形式如下:

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 语言中的死循环定义如下:

for {}

7. 指针

Go 语言可以使用指针,指针存储变量的地址,指针用*来定义。指针的定义和所指数据的类型相关:

var ap *int

这里的ap是指向整型数据的指针,&用于获取所变量的地址。

a :=12ap = &a

* 用于获取指针所指的地址的值。

fmt.Println(*ap)// => 12

指针通常用于将结构体做为参数传递。

  1. 传值通常意味着拷贝,意味着需要更多的内存。

  2. 使用指针传递时,在函数中改变的变量会传递给调用的方法或函数。

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

8. 函数

main 包中main函数是golang 程序的入口。我们可以定义多个函数并调用。例如:

package main
import ( "fmt" )
func add(a int, b int) int { c := a + b return c }
func main() { fmt.Println(add(2, 1)) } //=> 3

从上面的例子中我们可以看出,Golang 中的函数用func关键字加上函数名, 后面是附带数据类型的参数,最后是函数的返回类型。

函数的返回值可以被预先定义,例子如下:

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中声明。

你也可以定义一个多个返回值的函数,使用,进行分割。

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

8. 方法,结构体,接口

Golang 不是完全的面向对象语言,但是支持很多面向对象的特性,例如有结构体,接口,方法等。

8.1 结构体

结构体是有类型,不同变量的集合。例如我们想定义Person类型,其中包含姓名,年龄,性别。例如:

type person struct { name String age int gender string}

定义好了person结构体后,我们现在来使用它:

//方式 1: 指定属性和值p = person{name: "Bob", age: 42, gender: "Male"}//方式 2: 只指定值person{"Bob", 42, "Male"}

我们可以使用.符号访问这些属性:

p.name//=> Bobp.age//=> 42p.gender//=> Male

你也可是使用指针访问结构体的属性:

pp = &person{name: "Bob", age: 42, gender: "Male"}pp.name//=> Bob

8.2 方法

方法是一种带有接受器的特殊函数。接收器可以是值或者指针。例子如下:

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的接受器不是指针。

8.3 接口

Golang中的接口是方法的集合,接口有助于将同类型的属性组合起来,让我们一起来看一个anminal的接口。

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。我们把 snake和cat类型赋值给animal,使用Println 输出a.description。

我们在cat和snake中使用不同的方式实现了describe方法,我们得到了不同类型的输出。

9.包

在Golang中,我们的代码在某个包下。main包是程序执行的入口。在Go中有很多内置的包,例如我们之前用过的fmt包。

Go 的包机制是大型软件的基础,能够将大型的工程分解成小部分。 

—— Robert Griesemer

9.1 安装一个包

go get // 例子go get github.com/satori/go.uuid

安装的包保存在GOPATH的环境中,你可以在 $GOPATH/pkg 路径下看到安装的包。

9.2 创建一个自定义包

首先创建一个文件夹 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,需要注意的是在person包中函数secretName不能被访问,这是因为Go中小写字母开头的函数是私有函数。

9.3 生成包文档

Golang中有内置的功能支持包文档。运行下面的命令将生成文档:

godoc person Description

这将会为Description 函数生成文档,想要在web服务器上查看文档需要运行下面的命令:

godoc -http=":8080"

现在打开链接http://localhost:8080/pkg/将会看到我们刚才看到的文档。

9.4 Go 中内置的包

9.4.1 fmt

fmt包实现可标准的I/O函数,我们在之前的包中用过其中的打印输出函数。

9.4.2 json

Golang中另一个内置的重要包的是json,它能够对JSON进行编解码。

编码
package main
import ( "encoding/json" "fmt" )
func main() { mapA := map[string]int{"apple": 5, "lettuce": 7} mapB, _ := json.Marshal(mapA) fmt.Println(string(mapB)) }
解码
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中的“page”对应的是结构体中的PageNumber。

10. 错误处理

错误是程序中不应该出现的结果。假设我们编写一个API调用外部的服务。这个API可能成功也可能失败。当存在错误是,Golang程序能够识别:

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

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

10.1 从函数中返回自定义错误

当我们在自定义函数是,某些情况下会产生错误。我们可以使用error 对象返回这些错误:

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

Go 内置的包,外部的包都有处理错误的机制。因此我们调用的函数都有可能产生错误。这些错误不应该忽略而是应该向上面的例子那样被优雅的处理。

10.2 Panic

Panic是程序运行中突然产生未经处理的异常。在Go中,panic不是合理处理异常的方式,推荐使用error对象代替。当Panic产生时,程序将会暂停运行。当panic被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) }

10.3 Defer

Defer 在函数结尾一定会执行。在上面的函数中,使用panic() 暂停程序的运行,defer语句使程序执行结束时使用改行。Defer也可以用作我们想要在函数的结尾执行的语句,例如关闭文件。

11. 并发

Golong使用轻量级线程Go routies支持并发。

11.1 Go routine

Go routine 是能够并行运行的函数。创建Go routine 非常简单,只需要在函数前添加关键字go,这样函数就能够并行运行了。Go routines 是轻量级的,我们能够创建上千个Go routines。例如:

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,能够并行运行。我们想要在多线程中共享资源,但是Golang并不支持。因为这会导致死锁和资源等待。Go 提供了另一种共享资源的方式:channel。

11.2 Channels

我们可以使用Channel在两个Go routine之间传递数据。创建channel之前需要制定接受的数据类型。例如我们创建了一个接受string类型的channel。

c := make(chan string)

有了这个channel之后,我们可以通过这个channel发送和接收string类型的数据。

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

接收的channel一直等待发送的channel发送数据。

11.3 One way channel

有些情况下,我们希望Go routine 通过channel接收数据,但不发送数据,反之亦然。这时候我们可以创建一个one-way channel。例如:

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只能给channel发送数据而不能接受数据。

12. 使用selecct优化多个channel

有这样一种情况,一个函数等待多个channel,这时候我们可以使用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函数等待两个channel,c1和c2。使用select语句,先从channel中收到的数据会被打印出来。

12.1 Buffered channel

在Golang中可以创建buffered channel,当buffer满的时候,发送数据给channel将会被阻塞。例如:

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!

Golang为什么如此成功?

简单。。。—— Rob-pike

13. 总结

我们学习Golang以 下的主要模块和特性:

  1. 变量,数据类型

  2. Array,Slices和Map

  3. 函数

  4. 循环和条件语句

  5. 指针

  6. 方法,结构体和接口

  7. 错误处理

  8. 并发——Go routines和channels

恭喜你,你已经对Go有了很好的理解。

One of my most productive days was throwing away 1,000 lines of code.

—— Ken Thompson

不要停下脚步,继续前进。思考一个小应用程序并开始动手。


原文链接:

https://milapneupane.com.np/2019/07/06/learning-golang-from-zero-to-hero/


本文作者Milap Neupane,由何朋朋翻译。转载本文请注明出处,欢迎更多小伙伴加入翻译及投稿文章的行列,详情请戳公众号菜单「联系我们」。



参考阅读:



技术原创及架构实践文章,欢迎通过公众号菜单「联系我们」进行投稿。转载请注明来自高可用架构「ArchNotes」微信公众号及包含以下二维码。


高可用架构

改变互联网的构建方式

长按二维码 关注「高可用架构」公众号



    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存