最近写了很多Go代码,可是写着写着,仍是容易忘,尤为是再写点Python代码后。因此找了一篇不错的Golang基础教程,翻译一下,时常看看。git
原文连接: 「Learning Go — from zero to hero」 by Milap Neupanegithub
Go是由各类 包 组成的。main包是程序的入口,由它告诉编译器,这是一个可执行程序,而不是共享包。main包定义以下:golang
package main
Go的工做区是由环境变量GOPATH
决定的。
你能够在工做区里为所欲为地写代码,Go会在GOPATH
或者GOROOT
目录下搜索包。注:GOROOT
是Go的安装路径。json
设置GOPATH
为你想要的目录:数组
# export 环境变量 export GOPATH=~/workspace # 进入工做区目录 cd ~/workspace
在工做区目录里建立mian.go
文件。bash
package main import ( "fmt" ) func main(){ fmt.Println("Hello World!") }
咱们使用import
关键字来引入一个包。func main
是执行代码的入口,fmt是Go的内置包,主要用来格式化输入/输出。而Println是fnt中的一个打印函数。服务器
想要运行Go程序,有两种方法。并发
你们都知道,Go是一门编译型语言,因此在执行以前,咱们须要先编译它。app
> go build main.go
这个命令会生成二进制可执行文件 main,而后咱们再运行它。ide
> ./main # Hello World!
一个go run
命令就能够搞定。
go run main.go # Hello World!
注意:你能够在这个网站执行本文中的代码。
Go中的变量都是显式声明的。Go是静态语言,所以声明变量时,就会去检查变量的类型。
变量声明有如下三种方式。
# 1) a的默认值为0 var a int # 2) 声明并初始化a,a自动赋值为int var a = 1 # 3) 简写声明 message := "hello world"
还能够在一行声明多个变量
var b, c int = 2, 3
Go 支持的数字存储类型有不少,好比 int
, int8
, int16
, int32
, int64
,uint
, uint8
, uint16
, uint32
, uint64
, uintptr
等等。
字符串类型存储一个字节序列。使用string
关键字来声明。
布尔型使用bool
声明。
Go还支持复数类型数据类型,可使用complex64
和complex128
进行声明。
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)
数组是包含同一数据类型的元素序列,在声明时肯定数组长度,所以不能随意扩展。
数组的声明方式以下:
var a [5]int
多维数组的声明方式以下:
var multiD [2][3]int
Go中的数组有必定限制,好比不能修改数组长度、不能添加元素、不能获取子数组。这时候,更适合使用slice[分片]
这一类型。
分片用于存储一组元素,容许随时扩展其长度。分片的声明相似数组,只是去掉了长度声明。
var b []int
这行代码会建立一个 0容量、0长度的分片。也可使用如下代码 设置分片的容量和长度。
// 初始化一个长度为5,容量为10的分片 numbers := make([]int,5,10)
实际上,分片是对数组的抽象。分片使用数组做为底层结构。一个分片由三部分组成:容量、长度和指向底层数组的指针。
使用append
或者copy
方法能够扩大分片的容量。append
方法在分片的末尾追加元素,必要时会扩大分片容量。
numbers = append(numbers, 1, 2, 3, 4)
还可使用copy
方法来扩大容量。
// 建立一个更大容量的分片 number2 := make([]int, 15) // 把原分片复制到新分片 copy(number2, number)
如何建立一个分片的子分片呢?参考如下代码。
// 建立一个长度为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的一种数据类型,用于记录键值间的映射关系。使用如下代码建立一个map。
var m map[string]int // 新增 键/值 m['clearity'] = 2 m['simplicity'] = 3 // 打印值 fmt.Println(m['clearity']) // -> 2 fmt.Println(m['simplicity']) // -> 3
这里,m是一个键为string,值为int的map变量。
接下来看一下如何进行简单的类型转换。
a := 1.1 b := int(a) fmt.Println(b) //-> 1
并不是全部的数据类型都能转换成其余类型。注意:确保数据类型与转换类型相互兼容。
参考如下代码中的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用于组织多个条件语句,详看如下代码
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
循环。另外一种循环方式以下:
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
如下两种状况,一般优先选用指针。
好比:
func increment(i *int) { *i++ } func main() { i := 10 increment(&i) fmt.Println(i) } //=> 11
main
包中的main
函数是go程序执行的入口,除此之外,咱们还能够定义其余函数。
func add(a int, b int) int { c := a + b return c } func main() { fmt.Println(add(2, 1)) } //=> 3
如上所示,Go中使用func
关键字加上函数名来定义一个函数。函数的参数须要指明数据类型,最后是返回的数据类型。
函数的返回值也能够在函数中提早定义:
func add(a int, b int) (c int) { c = a + b return } func main() { fmt.Println(add(2, 1)) } //=> 3
这里c被定义为返回值,所以调用return
语句时,c会被自动返回。
你也能够一次返回多个变量:
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类型的数据进行分组,那么能够定义一我的的各类属性,包括姓名,年龄,性别等。
type person struct { name string age int gender string }
有了Person类型后,如今来建立一个 Person对象:
//方法 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
方法是一种带有接收器的函数。接收器能够是一个值或指针。咱们能够把刚刚建立的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
上。这样能够避免复制带来的内存消耗。
注意:上面示例中,age
被修改了,而name
不变。由于只有setAge
传入的是指针类型,能够对接收器进行修改。
在Go中,接口是方法的集合。接口能够对一个类型的属性进行分组,好比:
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。而后,给动物指定蛇和猫的类型,并打印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
go get <package-url-github> // 举个栗子 go get github.com/satori/go.uuid
包默认安装在GOPATH
环境变量设置的工做区中。可使用cd $GOPATH/pkg
命令进入目录,查看已安装的包。
首先建立一个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
注意:若是以上命令报错,确认一下
GO111MODULE
环境变量是否设置正确,参考连接。
而后回到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
如今,就能够引入包,并调用Description
方法了。注意,secretName
方法是小写字母开头的私有方法,因此不能被外部调用。
Go内置了对包文档的支持。运行如下命令生成文档:
go doc person Description
这将为person
包生成Description
函数的文档。请使用如下命令运行Web服务器,查看文档:
godoc -http=":8080"
打开这个连接http://localhost:8080/pkg/,就能看到文档了。
fmt
包实现了格式化I/O功能。咱们已经使用过这个包打印内容到标准输出流了。
另一个颇有用的包是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
使用Unmarshal
解码json字节时,第一个参数是json字节,第二个是指望解码后的结构体指针。注意:json:"page"
负责把page
映射到结构体中的PageNumber
字段上。
报错是程序中的意外产物。假如咱们正在使用API
调用一个外部服务。这个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) }
在写函数时,咱们可能会遇到须要报错的情景,这时能够返回一个自定义的error
对象。
func Increment(n int) (int, error) { if n < 0 { // return error object 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
。在Go中,更推荐使用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) }
Defer
语句老是在函数最后执行。
在上面的栗子中,咱们触发了panic
,可是defer
语句依然会在最后执行。Defer
适用于 须要在函数最后执行某些操做的场景,好比关闭文件。
Go在设计时考虑了并发性。 Go中的并发能够经过轻量级线程Go routines
来实现。
Go routine
是一个函数,它能够与另外一个函数并行或并发执行。 建立Go routine
很是简单,只需在函数前面添加关键字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
,与main函数中的线程并行。有时咱们想在多个线程之间共享资源。 Go倾向于不与另外一个线程共享变量,由于这会增长死锁和资源等待的可能。可是仙人自有妙招,就是接下来说到的go channel
。
咱们可使用channel
在两个routine
之间传递数据。建立channel
时,须要指定其接收的数据类型。
c := make(chan string)
经过上面建立的channel
,咱们能够发送/接收string
类型的数据。
package main import "fmt" func main(){ c := make(chan string) go func(){ c <- "hello" }() msg := <-c fmt.Println(msg) } //=>"hello"
接收方channel
会一直等待发送方发数据到channel
。
在某些场景下,咱们但愿Go routine
只接收数据但不发送数据,反之亦然。 这时,咱们能够建立一个单向channel
。
package main import ( "fmt" ) func main() { ch := make(chan string) go sc(ch) fmt.Println(<-ch) } // sc函数:只能发送数据给 channel,不能接收数据 func sc(ch chan<- string) { ch <- "hello" }
select
语句在Go routine
中处理多个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" } // => The first to arrive is: // => speed 2
在Go中,你还可使用缓冲区channel
,若是缓冲区已满,发送到该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
可以成功呢?
Simplicity… — Rob-pike
由于简单...
好了,本文终于结束了!你从菜鸟变成大佬了吗?开个玩笑,但愿看完能有所收获。