最近写了不少Go代码,但是写着写着,还是容易忘,尤其是再写点Python代码后。所以找了一篇不错的Golang基础教程,翻译一下,时常看看。

原文链接: 「Learning Go — from zero to hero」 by Milap Neupane

开始

Go是由各种 包 组成的。main包是程序的入口,由它告诉编译器,这是一个可执行程序,而不是共享包。main包定义如下:

  1. package main

工作区

Go的工作区是由环境变量GOPATH决定的。

你可以在工作区里随心所欲地写代码,Go会在GOPATH或者GOROOT目录下搜索包。注:GOROOT是Go的安装路径。

设置GOPATH为你想要的目录:

  1. # export 环境变量
  2. export GOPATH=~/workspace
  3. # 进入工作区目录
  4. cd ~/workspace

在工作区目录里创建main.go文件。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func main(){
  6. fmt.Println("Hello World!")
  7. }

我们使用import关键字来引入一个包。func main是执行代码的入口,fmt是Go的内置包,主要用来格式化输入/输出。而Printlnfmt中的一个打印函数。

想要运行Go程序,有两种方法。

方法一

大家都知道,Go是一门编译型语言,所以在执行之前,我们需要先编译它。

  1. > go build main.go

这个命令会生成二进制可执行文件 main,然后我们再运行它。

  1. > ./main
  2. # Hello World!

方法二

一个go run命令就可以搞定。

  1. go run main.go
  2. # Hello World!

注意:你可以在这个网站执行本文中的代码。

变量

Go中的变量都是显式声明的。Go是静态语言,因此声明变量时,就会去检查变量的类型。

变量声明有以下三种方式。

  1. # 1) a的默认值为0
  2. var a int
  3. # 2) 声明并初始化a,a自动赋值为int
  4. var a = 1
  5. # 3) 简写声明
  6. message := "hello world"

还可以在一行声明多个变量

  1. var b, c int = 2, 3

数据类型

数字,字符串 和 布尔型

Go 支持的数字存储类型有很多,比如 int, int8, int16, int32, int64,uint, uint8, uint16, uint32, uint64, uintptr 等等。

字符串类型存储一个字节序列。使用string关键字来声明。

布尔型使用bool声明。

Go还支持复数类型数据类型,可以使用complex64complex128进行声明。

  1. var a bool = true
  2. var b int = 1
  3. var c string = 'hello world'
  4. var d float32 = 1.222
  5. var x complex128 = cmplx.Sqrt(-5 + 12i)

数组, 分片 和 映射Map

数组是包含同一数据类型的元素序列,在声明时确定数组长度,因此不能随意扩展。

数组的声明方式如下:

  1. var a [5]int

多维数组的声明方式如下:

  1. var multiD [2][3]int

Go中的数组有一定限制,比如不能修改数组长度、不能添加元素、不能获取子数组。这时候,更适合使用slice[分片]这一类型。

分片用于存储一组元素,允许随时扩展其长度。分片的声明类似数组,只是去掉了长度声明。

  1. var b []int

这行代码会创建一个 0容量、0长度的分片。也可以使用以下代码 设置分片的容量和长度。

  1. // 初始化一个长度为5,容量为10的分片
  2. numbers := make([]int,5,10)

实际上,分片是对数组的抽象。分片使用数组作为底层结构。一个分片由三部分组成:容量、长度和指向底层数组的指针。

使用append或者copy方法可以扩大分片的容量。append方法在分片的末尾追加元素,必要时会扩大分片容量。

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

还可以使用copy方法来扩大容量。

  1. // 创建一个更大容量的分片
  2. number2 := make([]int, 15)
  3. // 把原分片复制到新分片
  4. copy(number2, number)

如何创建一个分片的子分片呢?参考以下代码。

  1. // 创建一个长度为4的分片
  2. number2 = []int{1,2,3,4}
  3. fmt.Println(numbers) // -> [1 2 3 4]
  4. // 创建子分片
  5. slice1 := number2[2:]
  6. fmt.Println(slice1) // -> [3 4]
  7. slice2 := number2[:3]
  8. fmt.Println(slice2) // -> [1 2 3]
  9. slice3 := number2[1:4]
  10. fmt.Println(slice3) // -> [2 3 4]

Map也是Go的一种数据类型,用于记录键值间的映射关系。使用以下代码创建一个map。

  1. var m map[string]int
  2. // 新增 键/值
  3. m['clearity'] = 2
  4. m['simplicity'] = 3
  5. // 打印值
  6. fmt.Println(m['clearity']) // -> 2
  7. fmt.Println(m['simplicity']) // -> 3

这里,m是一个键为string,值为int的map变量。

类型转换

接下来看一下如何进行简单的类型转换。

  1. a := 1.1
  2. b := int(a)
  3. fmt.Println(b)
  4. //-> 1

并非所有的数据类型都能转换成其他类型。注意:确保数据类型与转换类型相互兼容。

条件语句

if else

参考以下代码中的if-else语句进行条件判断。注意:花括号与条件语句要在同一行。

  1. if num := 9; num < 0 {
  2. fmt.Println(num, "is negative")
  3. } else if num < 10 {
  4. fmt.Println(num, "has 1 digit")
  5. } else {
  6. fmt.Println(num, "has multiple digits")
  7. }

switch case

switch-case用于组织多个条件语句,详看以下代码

  1. i := 2
  2. switch i {
  3. case 1:
  4. fmt.Println("one")
  5. case 2:
  6. fmt.Println("two")
  7. default:
  8. fmt.Println("none")
  9. }

循环

Go中用于循环的关键字只有一个for

  1. i := 0
  2. sum := 0
  3. for i < 10 {
  4. sum += 1
  5. i++
  6. }
  7. fmt.Println(sum)

以上代码类似于C语言中的while循环。另一种循环方式如下:

  1. sum := 0
  2. for i := 0; i < 10; i++ {
  3. sum += i
  4. }
  5. fmt.Println(sum)

Go中的死循环

  1. for {
  2. }

指针

Go提供了指针,用于存储值的地址。指针使用*来声明。

  1. var ap *int

这里的ap变量即指向整型的指针。使用&运算符获取变量地址,*运算符用来获取指针所指向的值。

  1. a := 12
  2. ap = &a
  3. fmt.Println(*ap)
  4. // => 12

以下两种情况,通常优先选用指针。

  • 把结构体作为参数传递时。因为值传递会耗费更多内存。
  • 声明某类型的方法时。传递指针后,方法/函数可以直接修改指针所指向的值。

比如:

  1. func increment(i *int) {
  2. *i++
  3. }
  4. func main() {
  5. i := 10
  6. increment(&i)
  7. fmt.Println(i)
  8. }
  9. //=> 11

函数

main包中的main函数是go程序执行的入口,除此以外,我们还可以定义其他函数。

  1. func add(a int, b int) int {
  2. c := a + b
  3. return c
  4. }
  5. func main() {
  6. fmt.Println(add(2, 1))
  7. }
  8. //=> 3

如上所示,Go中使用func关键字加上函数名来定义一个函数。函数的参数需要指明数据类型,最后是返回的数据类型。

函数的返回值也可以在函数中提前定义:

  1. func add(a int, b int) (c int) {
  2. c = a + b
  3. return
  4. }
  5. func main() {
  6. fmt.Println(add(2, 1))
  7. }
  8. //=> 3

这里c被定义为返回值,因此调用return语句时,c会被自动返回。

你也可以一次返回多个变量:

  1. func add(a int, b int) (int, string) {
  2. c := a + b
  3. return c, "successfully added"
  4. }
  5. func main() {
  6. sum, message := add(2, 1)
  7. fmt.Println(message)
  8. fmt.Println(sum)
  9. }

方法、结构体和接口

Go 不是完全面向对象的语言,但是有了 方法、结构体和接口,它也可以达到面向对象的效果。

Struct 结构体

结构体包含不同类型的字段,可用来对数据进行分组。例如,如果我们要对Person类型的数据进行分组,那么可以定义一个人的各种属性,包括姓名,年龄,性别等。

  1. type person struct {
  2. name string
  3. age int
  4. gender string
  5. }

有了Person类型后,现在来创建一个 Person对象:

  1. //方法 1: 指定参数和值
  2. p = person{name: "Bob", age: 42, gender: "Male"}
  3. //方法 2: 仅指定值
  4. person{"Bob", 42, "Male"}

可以使用.来获取一个对象的参数。

  1. p.name
  2. //=> Bob
  3. p.age
  4. //=> 42
  5. p.gender
  6. //=> Male

也可以通过结构体的指针对象来获取参数。

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

方法

方法是一种带有接收器的函数。接收器可以是一个值或指针。我们可以把刚刚创建的Person类型作为接收器来创建方法:

  1. package main
  2. import "fmt"
  3. // 定义结构体
  4. type person struct {
  5. name string
  6. age int
  7. gender string
  8. }
  9. // 定义方法
  10. func (p *person) describe() {
  11. fmt.Printf("%v is %v years old.", p.name, p.age)
  12. }
  13. func (p *person) setAge(age int) {
  14. p.age = age
  15. }
  16. func (p person) setName(name string) {
  17. p.name = name
  18. }
  19. func main() {
  20. pp := &person{name: "Bob", age: 42, gender: "Male"}
  21. // 使用 . 来调用方法
  22. pp.describe()
  23. // => Bob is 42 years old
  24. pp.setAge(45)
  25. fmt.Println(pp.age)
  26. //=> 45
  27. pp.setName("Hari")
  28. fmt.Println(pp.name)
  29. //=> Bob
  30. }

注意,此处的接收器是一个指针,方法中对指针进行的任何修改,都可以反映在接收器pp上。这样可以避免复制带来的内存消耗。

注意:上面示例中,age被修改了,而name不变。因为只有setAge传入的是指针类型,可以对接收器进行修改。

接口

在Go中,接口是方法的集合。接口可以对一个类型的属性进行分组,比如:

  1. type animal interface {
  2. description() string
  3. }

animal是一个接口。通过实现animal接口,我们来创建两种不同类型的动物。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. type animal interface {
  6. description() string
  7. }
  8. type cat struct {
  9. Type string
  10. Sound string
  11. }
  12. type snake struct {
  13. Type string
  14. Poisonous bool
  15. }
  16. func (s snake) description() string {
  17. return fmt.Sprintf("Poisonous: %v", s.Poisonous)
  18. }
  19. func (c cat) description() string {
  20. return fmt.Sprintf("Sound: %v", c.Sound)
  21. }
  22. func main() {
  23. var a animal
  24. a = snake{Poisonous: true}
  25. fmt.Println(a.description())
  26. a = cat{Sound: "Meow!!!"}
  27. fmt.Println(a.description())
  28. }
  29. //=> Poisonous: true
  30. //=> Sound: Meow!!!

在main函数中,我们创建了一个类型为animal的变量a。然后,给动物指定蛇和猫的类型,并打印a.description

在Go中,所有的代码都写在包里面。main包是程序执行的入口,Go自带了很多内置包,最有名的就是刚刚用过的fmt包。

“Go packages in the main mechanism for programming in the large that go provides and they make possible to divvy up a large project into smaller pieces.”

— Robert Griesemer

安装一个包

  1. go get <package-url-github>
  2. // 举个栗子
  3. go get github.com/satori/go.uuid

包默认安装在GOPATH环境变量设置的工作区中。可以使用cd $GOPATH/pkg命令进入目录,查看已安装的包。

自定义包

首先创建一个custom_package文件夹

  1. > mkdir custom_package
  2. > cd custom_package

假设要创建一个person包,首先在custom_package目录下创建一个person文件夹。

  1. > mkdir person
  2. > cd person

然后创建一个 person.go文件

  1. package person
  2. func Description(name string) string {
  3. return "The person name is: " + name
  4. }
  5. func secretName(name string) string {
  6. return "Do not share"
  7. }

现在需要安装这个包,以便引入并使用它。

  1. > go install

注意:如果以上命令报错,确认一下GO111MODULE环境变量是否设置正确,参考链接

然后回到custom_package目录下,创建一个main.go文件。

  1. package main
  2. import(
  3. "custom_package/person"
  4. "fmt"
  5. )
  6. func main(){
  7. p := person.Description("Milap")
  8. fmt.Println(p)
  9. }
  10. // => The person name is: Milap

现在,就可以引入包,并调用Description方法了。注意,secretName方法是小写字母开头的私有方法,所以不能被外部调用。

包的文档

Go内置了对包文档的支持。运行以下命令生成文档:

  1. go doc person Description

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

  1. godoc -http=":8080"

打开这个链接http://localhost:8080/pkg/,就能看到文档了。

Go中的一些内置包

fmt

fmt包实现了格式化I/O功能。我们已经使用过这个包打印内容到标准输出流了。

json

另外一个很有用的包是json,用来编码/解码Json数据。

  1. // 编码
  2. package main
  3. import (
  4. "fmt"
  5. "encoding/json"
  6. )
  7. func main(){
  8. mapA := map[string]int{"apple": 5, "lettuce": 7}
  9. mapB, _ := json.Marshal(mapA)
  10. fmt.Println(string(mapB))
  11. }
  1. // 解码
  2. package main
  3. import (
  4. "fmt"
  5. "encoding/json"
  6. )
  7. type response struct {
  8. PageNumber int `json:"page"`
  9. Fruits []string `json:"fruits"`
  10. }
  11. func main(){
  12. str := `{"page": 1, "fruits": ["apple", "peach"]}`
  13. res := response{}
  14. json.Unmarshal([]byte(str), &res)
  15. fmt.Println(res.PageNumber)
  16. }
  17. //=> 1

使用Unmarshal解码json字节时,第一个参数是json字节,第二个是期望解码后的结构体指针。注意:json:"page"负责把page映射到结构体中的PageNumber字段上。

错误处理

报错是程序中的意外产物。假如我们正在使用API调用一个外部服务。这个API调用可能成功,也可能失败。比如,可以使用以下方法,处理报错:

  1. package main
  2. import (
  3. "fmt"
  4. "net/http"
  5. )
  6. func main(){
  7. resp, err := http.Get("http://example.com/")
  8. if err != nil {
  9. fmt.Println(err)
  10. return
  11. }
  12. fmt.Println(resp)
  13. }

返回自定义错误

在写函数时,我们可能会遇到需要报错的情景,这时可以返回一个自定义的error对象。

  1. func Increment(n int) (int, error) {
  2. if n < 0 {
  3. // return error object
  4. return nil, errors.New("math: cannot process negative number")
  5. }
  6. return (n + 1), nil
  7. }
  8. func main() {
  9. num := 5
  10. if inc, err := Increment(num); err != nil {
  11. fmt.Printf("Failed Number: %v, error message: %v", num, err)
  12. }else {
  13. fmt.Printf("Incremented Number: %v", inc)
  14. }
  15. }

大部分的内置包或者外部包,都有自己的报错处理机制。因此我们使用的任何函数可能报错,这些报错都不应该被忽略,应该像上面示例中,在调用函数的地方,优雅地处理报错。

Panic

当程序在运行过程中,突然遇到了未处理的报错,就会导致panic。在Go中,更推荐使用error对象,而不是panic来处理异常。发生panic后,程序会停止运行,但会运行defer语句代码。

  1. //Go
  2. package main
  3. import "fmt"
  4. func main() {
  5. f()
  6. fmt.Println("Returned normally from f.")
  7. }
  8. func f() {
  9. defer func() {
  10. if r := recover(); r != nil {
  11. fmt.Println("Recovered in f", r)
  12. }
  13. }()
  14. fmt.Println("Calling g.")
  15. g(0)
  16. fmt.Println("Returned normally from g.")
  17. }
  18. func g(i int) {
  19. if i > 3 {
  20. fmt.Println("Panicking!")
  21. panic(fmt.Sprintf("%v", i))
  22. }
  23. defer fmt.Println("Defer in g", i)
  24. fmt.Println("Printing in g", i)
  25. g(i + 1)
  26. }

Defer

Defer语句总是在函数最后执行。

在上面的栗子中,我们触发了panic,但是defer语句依然会在最后执行。Defer适用于 需要在函数最后执行某些操作的场景,比如关闭文件。

并发

Go在设计时考虑了并发性。 Go中的并发可以通过轻量级线程Go routines来实现。

Go routine

Go routine是一个函数,它可以与另一个函数并行或并发执行。 创建Go routine非常简单,只需在函数前面添加关键字go,就可以使其并行执行。 同时,它很轻量级,因此可以创建上千个routine

  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. )
  6. func main() {
  7. go c()
  8. fmt.Println("I am main")
  9. time.Sleep(time.Second * 2)
  10. }
  11. func c() {
  12. time.Sleep(time.Second * 2)
  13. fmt.Println("I am concurrent")
  14. }
  15. //=> I am main
  16. //=> I am concurrent

上面的示例中,c函数是一个Go routine,与main函数中的线程并行。有时我们想在多个线程之间共享资源。 Go倾向于不与另一个线程共享变量,因为这会增加死锁和资源等待的可能。但是仙人自有妙招,就是接下来讲到的go channel

Channels

我们可以使用channel在两个routine之间传递数据。创建channel时,需要指定其接收的数据类型。

  1. c := make(chan string)

通过上面创建的channel,我们可以发送/接收string类型的数据。

  1. package main
  2. import "fmt"
  3. func main(){
  4. c := make(chan string)
  5. go func(){ c <- "hello" }()
  6. msg := <-c
  7. fmt.Println(msg)
  8. }
  9. //=>"hello"

接收方channel会一直等待发送方发数据到channel

单向channel

在某些场景下,我们希望Go routine只接收数据但不发送数据,反之亦然。 这时,我们可以创建一个单向channel

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func main() {
  6. ch := make(chan string)
  7. go sc(ch)
  8. fmt.Println(<-ch)
  9. }
  10. // sc函数:只能发送数据给 channel,不能接收数据
  11. func sc(ch chan<- string) {
  12. ch <- "hello"
  13. }

使用select语句在Go routine中处理多个channel

一个函数可能正在等待多个通道。这时,我们可以使用select语句。

  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. )
  6. func main() {
  7. c1 := make(chan string)
  8. c2 := make(chan string)
  9. go speed1(c1)
  10. go speed2(c2)
  11. fmt.Println("The first to arrive is:")
  12. select {
  13. case s1 := <-c1:
  14. fmt.Println(s1)
  15. case s2 := <-c2:
  16. fmt.Println(s2)
  17. }
  18. }
  19. func speed1(ch chan string) {
  20. time.Sleep(2 * time.Second)
  21. ch <- "speed 1"
  22. }
  23. func speed2(ch chan string) {
  24. time.Sleep(1 * time.Second)
  25. ch <- "speed 2"
  26. }
  27. // => The first to arrive is:
  28. // => speed 2

Buffered channel

在Go中,你还可以使用缓冲区channel,如果缓冲区已满,发送到该channel的消息将被阻塞。

  1. package main
  2. import "fmt"
  3. func main(){
  4. ch := make(chan string, 2)
  5. ch <- "hello"
  6. ch <- "world"
  7. ch <- "!" // extra message in buffer
  8. fmt.Println(<-ch)
  9. }
  10. // => fatal error: all goroutines are asleep - deadlock!

最后唠唠嗑

为什么 Golang 能够成功呢?

Simplicity… — Rob-pike

因为简单...

好了,本文终于结束了!你从菜鸟变成大佬了吗?开个玩笑,希望看完能有所收获。

Golang快速入门:从菜鸟变大佬的更多相关文章

  1. Golang快速入门

    Go语言简介: Golang 简称 Go,是一个开源的编程语言,Go是从2007年末由 Robert Griesemer, Rob Pike, Ken Thompson主持开发,后来还加入了Ian L ...

  2. golang快速入门(四)

    提示:本系列文章适合有其他语音基础并对Go有持续冲动的读者 一.golang获取HTTP请求 1.在golang标准库中提供了net包来处理网络连接,通过http.Get创建http请求并返回服务器响 ...

  3. golang快速入门(五)初尝web服务

    提示:本系列文章适合对Go有持续冲动的读者 初探golang web服务 golang web开发是其一项重要且有竞争力的应用,本小结来看看再golang中怎么创建一个简单的web服务. 在不适用we ...

  4. golang快速入门(六)特有程序结构

    提示:本系列文章适合对Go有持续冲动的读者 阅前须知:在程序结构这章,更多会关注golang中特有结构,与其他语言如C.python中相似结构(命名.声明.赋值.作用域等)不再赘述. 一.golang ...

  5. golang快速入门(练习)

    1.打包和工具链 1.1 包 所有 Go 语言的程序都会组织成若干组文件,每组文件被称为一个包. ? 1 2 3 4 5 6 7 8 9 10 net/http/     cgi/     cooki ...

  6. Nginx快速入门菜鸟笔记

    Nginx快速入门-菜鸟笔记   1.编译安装nginx 编译安装nginx 必须先安装pcre库. (1)uname -a 确定环境 Linux localhost.localdomain 2.6. ...

  7. Golang Module快速入门

    前言: 在Golang1.11之前的版本中,官方没有提供依赖和包管理工具.开发者通常会使用vendor或者glide的方式来管理依赖(也有直接使用GOPATH多环境方式),而在Golang1.11之后 ...

  8. 菜鸟系列k8s——k8s快速入门(1)

    k8s快速入门 1.快速创建k8s集群 参考网站:https://kubernetes.io/docs/tutorials/kubernetes-basics 点击教程菜单 1. Create a C ...

  9. Docker快速入门(二)

    上篇文章<Docker快速入门(一)>介绍了docker的基本概念和image的相关操作,本篇将进一步介绍image,容器和Dockerfile. 1 image文件 (1)Docker ...

随机推荐

  1. Mac word文档的消失问题以及解决方案

    最近用mac电脑上的Microsoft Word写文档时,出现一个很奇怪的现象:明明我已经保存了文档到某个目录下,但是当我退出Word后,准备去保存目录找文档时发现文档消失了,前一秒还在!!! 通过各 ...

  2. 「MoreThanJava」计算机发展史—从织布机到IBM

    「MoreThanJava」 宣扬的是 「学习,不止 CODE」,本系列 Java 基础教程是自己在结合各方面的知识之后,对 Java 基础的一个总回顾,旨在 「帮助新朋友快速高质量的学习」. 当然 ...

  3. 使用Shiro+JWT完成的微信小程序的登录(含讲解)

    使用Shiro+JWT完成的微信小程序的登录 源码地址https://github.com/Jirath-Liu/shiro-jwt-wx 微信小程序用户登陆,完整流程可参考下面官方地址,本例中是按此 ...

  4. div实现富文本编辑框

    ocument.execCommand()方法处理Html数据时常用语法格式如下:document.execCommand(sCommand[,交互方式, 动态参数]) 其中:sCommand为指令参 ...

  5. Shell:Day04.笔记

    grep与正则表达式: 1.grep程序 Linux下有文本处理三剑客 - - grep sed awk grep:文本 行 过滤工具 sed:文本 行 编辑器(流编辑器) awk:报告生成器(做文本 ...

  6. 怎么用scratch做大鱼吃小鱼

    行走代码不说了.出鱼代码大概就是 棋子被点击时 重复执行 移到x:从()到()任意选一个数,y一样 克隆自己 等待你想要的秒数.吃鱼代码就是 当作为克隆体启动是 重复执行 如果碰到()那么 删除克隆体 ...

  7. 【数据库】MySQL数据库(二)

    一.数据库文件的导出 1.在DOS命令行下导出数据库(带数据) mysqldump -u root -p 数据库名 > E:\wamp\www\lamp175\lamp175.sql 2.在DO ...

  8. MySQL操作表的约束

    完整性:指数据库的准确性和一致性. 约束:是在表中定义的用于维护数据库完整性的一些规则. 主键:给某一个字段来唯一标识所有记录,值是唯一的,非空的 外键:多个表之间参照的完整性. 一.设置非空约束 u ...

  9. python 递归、匿名函数、

    1.递归:就是函数自己调用自己.(注:递归最多循环999) 2.匿名函数(意义:减少内存占用) lambada 定义一个匿名函数,eg:lambad x,b:x+b  (:前面是入参eg:x,b,:后 ...

  10. Go gRPC进阶-TLS认证+自定义方法认证(七)

    前言 前面篇章的gRPC都是明文传输的,容易被篡改数据.本章将介绍如何为gRPC添加安全机制,包括TLS证书认证和Token认证. TLS证书认证 什么是TLS TLS(Transport Layer ...