golang快速入门
此文适合有一定语言基础的读者
go简介
语言哲学
C语言是纯过程式的,这和它产生的历史背景有关。Java语言则是激进的面向对象主义推崇者,典型表现是它不能容忍体系里存在孤立的函数。而Go语言没有去否认任何一方,而是用批判吸收的眼光,将所有编程思想做了一次梳理,融合众家之长,但时刻警惕特性复杂化,极力维持语言特性的简洁,力求小而精
Go语言反对函数和操作符重载(overload),而C++、Java和C#都允许出现同名函数或操作符,只要它们的参数列表不同。
其次,Go语言支持类、类成员方法、类的组合,但反对继承,反对虚函数(virtual function)和虚函数重载。确切地说,Go也提供了继承,只不过是采用了组合的文法来提供
1type Foo struct {
2 Base // struct类型
3 ...
4}
5func (foo *Foo) Bar() {
6 ...
7}
Go语言也放弃了构造函数(constructor)和析构函数(destructor)
在放弃了大量的OOP特性后,Go语言送上了一份非常棒的礼物:接口(interface)。Go语言中的接口与其他语言最大的一点区别是它的非侵入性。
Go语言实现类的时候无需从接口派生,只要实现的接口定义的方法就表示实现了该接口。
以上摘自许式伟的《Go语言编程》
优点
- 够简单
- 语言级别支持并发编程
- 错误处理新规范
- 自动垃圾回收
- 匿名函数与闭包
- 类型与接口
- 反射
- 语言交互(主要与c)
- 丰富的内置类型
缺点
- 错误处理比较蛋疼,需要写很多err判断
一、hello golang!
1package main
2import "fmt"
3func main() {
4 fmt.Println("hello golang!")
5}
使用任意编辑器编写保存,命令行中:go run hello.go即可运行 可以在https://play.golang.org 上直接运行,不需要安装环境
关键字
关键字 | 作用 |
---|---|
package | 代码所属包 |
import | 导入依赖包,不能导入不使用的包 |
main | 主函数入口,无参数无返回值,命令行参数保存在os.Args中 |
func | 函数声明 |
go | 开启协程(并发核心) |
map | 字典类型, map[string]bool |
delete | 专用来删除字典中的元素 |
chan | 通道,协程通信核心 |
select | 同时读取多个chan的信息 |
close | 用来关闭通道 |
make | 用来创建chan或map |
type | 类型定义,定义任意需要的类型 |
struct | C中的结构体,但可以定义方法以实现类功能 |
interface | 接口类型,用来定义接口 |
new | 新建对象, 并获得它的指针 |
range | 类似python的range,配合for遍历列表、数组或map的元素 |
defer | 自动关闭资源,退出代码体(一般是函数)时执行 |
error | error接口,只要实现Error()string方法就是一个error接口 |
panic | 抛出异常,如果不用recover捕获则会终止程序 |
recover | 捕获panic异常,防止程序终止,与recover结合 |
数值定义 | — |
const | 常量声明 |
var | 变量声明 |
数值类型 | — |
bool | 布尔 |
string | 字符 |
int,int8,int16,int32,int64 | int长度与平台相关 |
uint,uint8,uint16,uint32,uint64 | uint长度与平台相关 |
uintptr | 同指针,32位平台为4字节,64位八字节 |
byte | 等价于uint8 |
rune | 等价于uint32,单个unicode字符 |
float32,float64 | |
complex64,complex128 | 复数类型, value = 3.2+12i |
操作符
赋值:=,:=
数值运算:+,-,*,/,%
比较运算:>,<,==,>=,<=,!=
位运行:»,«,^,&,|与^x(取反)
特殊操作符 | 作用 |
---|---|
:= | 无需指定类型即可赋值,i, j := true, “hello” |
_ | **可以赋任意值的空对象,*_ = “string”*** |
二、语法快速过滤
与其他语言比较
注释:Go程序的代码注释与C++保持一致,支付块注释与行注释
1/*
2块注释
3*/
4// 行注释
无java/c中的结束分号:;
package
类似java的包,一般与目录同名,只有运行目录时包名必须为main,一个目录不能出现多个包名
import
1import (
2 "fmt"
3 mr "math/rand"
4 _ "encoding/json"
5)
- mr 是”math/rand”的别名,可用来避免重名/长名
- _ 表示初始化包,但并不使用
也可以
1import "fmt"
2import mr "math/rand"
3import _ "encoding/json"
明显上一种能减少不少代码量
注意:只有大写字母开头的方法或对象可以被导出使用,类似公有私有概念
const
1const Pi = 3.14 // 默认类型为float64
2const x,y int = 1,2 // 指定类型
3// 也可以集中定义
4const(
5 Big = 1 << 100
6 Small = Big >> 99
7)
8// 如果为同样的值,可省略不写
9const (
10 a = 1
11 b // b = 1
12 c // c = 1
13 d // d = 1
14)
15// 可以使用iota加1,iota从0开始
16const (
17 a = 1 + iota
18 b // b = 2
19 c // c = 3
20 d // d = 4
21)
22// 遇到const关键字置0
23const (
24 e = iota // e = 0
25)
var
1var i int
2var x,y,z int // 声明多个变量
3var x,y,z int = 1,2,3 // 声明多个变量并初始化,int可以省略
4var i,j = true, "hello" // 多变量类型同时赋值
5var( // 集中声明,与导包方式一样
6 v1 int
7 v2 string
8)
9v3 := "no var init" // 只能在函数体内使用此方法
10x,y = y,x // 多重赋值
11_,i,_ = x,y,z // 使用'_'屏蔽不需要的值
- 注意go变量名放在类型之前,这个与c,java相反,对同类型变量可以只留下最后一个类型声名
- 注意不能使用
var i bool,j string = true, "hello"
进行赋值
map
1var myMap map[string]string // 字典声名,声明的map是空值,需要用make()创建
2myMap = make(map[string]string)
func
1func add(x int,y int) int{ // 这里也可以写成add(x,y int)
2 return x + y
3}
4
5// 函数内声明的变量必须被使用,否则会报错
6func nouse(){
7 var no string
8 // 变量未使用报错
9 // 可以使用'_ = no'来避免报错
10}
11
12// 可以是空函数体
13func empty(){
14}
15
16// 可变参数
17func vary(args... int) {
18 fmt.Println(args)
19}
20// vary() -> [] 可以为空
21// vary(1) -> [1]
22// vary(1,2,3) -> [1 2 3]
23
24// 混合使用固定参数与可变参数,可变参数放在最后,不能与固定参数共用参数类型
25func fix_vary(fix int, args...int) { // fix可以不使用
26 fmt.Println(args)
27}
28
29// 多值返回, 这个函数可以直接用多重赋值实现x,y = y,x
30func swap(x, y int) (int,int){
31 return y, x
32}
33
34// 返回值指定变量名
35func split(sum int) (x,y int) {
36 x = sum / 5
37 y = sum % 5
38 return // 当指定变量名时,返回可以略去不写
39}
40
41// 匿名函数
42func(x,y int)int{
43 return x+y
44}
45// 匿名函数直接执行, 直接在函数定义后面加上参数即可
46func(x,y int)int{
47 return x+y
48}(2,3) // 传入参数2,3运行
49// 匿名函数赋值
50f := func(x,y int)int{
51 return x+y
52}
流程控制中的条件表达式,不需要小括号
if else, goto
1// 普通用法同其他语言
2// 特殊用法, 可以初始化变量
3if i:=j;i>0{
4 // ... do sth about i
5}
6
switch,case,select
switch不需要用break来退出一个case,默认退出
1switch i {
2case 0:
3 // ...
4case 1:
5 // ...
6// ...
7default:
8 // ...
9}
10// switch后面的表达式不是必须的
11switch{
12case char == 'a':
13 // ...
14case char == 'b':
15 // ...
16}
17// 单个case可出现多个可选结果
18switch 1 {
19case 1,2,3:
20 // ...
21default:
22 // ...
23}
24// 可以初始化变量,可以不做任何处理
25switch os:=runtime.GOOS;os{
26case "darwin":
27case "linux":
28}
for … range()
1// 常规使用同c
2// 死循环
3for{
4 // do something
5}
6// 多重赋值, 实现反序
7a := []int{1, 5, 3, 4, 2, 6}
8for i, j := 0, len(a) - 1; i < j; i, j = i + 1, j - 1 {
9 a[i], a[j] = a[j], a[i]
10}
11fmt.Println(a) // [6 2 4 3 5 1]
12
13// 遍历列表
14for index,value := range(alist){
15 // ...
16}
17// 遍历字典
18for key,value := range(amap){
19 // ...
20}
defer
1f,err := os.Open(filename)
2if err != nil{
3 log.Println("")
4 return
5}
6defer f.Close() // 程序处理完后续代码退出或异常退出时执行
7// ... 其他处理
三、struct,另类的类
给类型加上方法便是对象,用type定义自己的类型,然后加上方法
1type Myint int
2func (m Myint)Less(b Myint) bool{
3 return m < b
4}
5var a Myint = 1
6if a.Less(2){ // 对天显示的类型2会自动处理成相应类型
7 // do something
8}
因为一般对象有不少属性,所以一般用struct来定义对象
1type Person struct{
2 Name string
3 Age int
4}
5func (p Person)GetName()string{
6 return p.Name
7}
8// 需要修改对象成员时用指针
9func (p *Person)SetName(name string){
10 p.Name = name
11}
对象继承,c中的struct匿名组合
1type Base struct{
2 Name string
3}
4func (b *Base) Name()string{...}
5type Foo struct{
6 Base //匿名实现继承,Base中的方法,Foo可以直接使用
7 ...
8}
9var foo Foo
10foo.Name() // Name()为Base中的方法
可见性,没有private,protected,public,只有大写开头的成员才能导出包外被使用
四、接口
用interface定义,接口不需要被继承,任何对象只要实现了接口中的方法就实现了此接口
1type IFile interface {
2 Read(buf []byte) (n int, err error)
3 Write(buf []byte) (n int, err error)
4 Seek(off int64, whence int) (pos int64, err error)
5 Close() error
6}
7type IReader interface {
8 Read(buf []byte) (n int, err error)
9}
10type IWriter interface {
11 Write(buf []byte) (n int, err error)
12}
13type ICloser interface {
14 Close() error
15}
16// 以上实现了四个接口, 下面定义一个File对象
17type File struct {
18 // ...
19}
20func (f *File) Read(buf []byte) (n int, err error)
21func (f *File) Write(buf []byte) (n int, err error)
22func (f *File) Seek(off int64, whence int) (pos int64, err error)
23func (f *File) Close() error
24
25var file1 IFile = new(File)
26var file2 IReader = new(File)
27var file3 IWriter = new(File)
28var file4 ICloser = new(File)
29// 因为File实现了所有接口的方法,所有File可以赋值给四个接口变量
接口查询,查看对象是否实现接口 (使用类型判断方法)
1var file1 Writer = ...
2if file5, ok := file1.(two.IStream); ok {
3 // ...
4}
接口组合,可以像struct一样组合成一个新接口
1type ReadWriter interface {
2IReader
3IWriter
4}
5// 完全等同于下面写法
6type ReadWriter interface {
7 Read(p []byte) (n int, err error)
8 Write(p []byte) (n int, err error)
9}
空接口interface{},可以接受任何类型
1{
2 var v1 interface{} = 1 // int interface{}
3 var v2 interface{} = "abc" // string interface{}
4 var v3 interface{} = &v2 // *interface{} interface{}
5 var v4 interface{} = struct{ X int }{1}
6 var v5 interface{} = &struct{ X int }{1}
7}
五、类型与转换
类型查询,使用.(target type)来判断类型
1var v1 interface{} = ...
2switch v := v1.(type) {
3case int:
4 // v为 int
5case string:
6 // v为 string
7//...
8}
9
10tes := make(map[string]interface{})
11tes["a"] = "abc"
12tes["b"] = 123
13
14value, ok := tes["a"].(string) // "abc", true
15value2,ok2 := tes["a"].(int) // "", false
类型强转,只有兼容的对象可以进行转换
1var var1 int = 7
2var2 := float64(var1)
3var3 := int64(var1)
4
5var4 := new(int32) // var4为指针变量
6var5 := (*int32)(var4)
六、并发
关键字go简单暴力实现并发,不同于线程/进程,更轻量级的协程
1package main
2import "fmt"
3func Add(x, y int) {z := x + y
4 fmt.Println(z) }
5func main() {
6 for i := 0; i < 10; i++ {
7 go Add(i, i) // 并发计算
8 }
9}
10// 打印顺序如下,若要console能显示,需要加time.Sleep(),否则没来得及打印就退出了
11// 4
12// 6
13// 8
14// 10
15// 2
16// 14
17// 12
18// 16
19// 0
20// 18
并发通信
1// c思路go实现
2package main
3import "fmt"
4import "sync"
5import "runtime"
6
7var counter int = 0
8func Count(lock *sync.Mutex) {
9 lock.Lock()
10 counter++
11 fmt.Println(counter)
12 lock.Unlock()
13}
14func main() {
15 lock := &sync.Mutex{}
16 for i := 0; i < 10; i++ {
17 go Count(lock)
18 }
19 for {
20 lock.Lock()
21 c := counter
22 lock.Unlock()
23 runtime.Gosched()
24 if c >= 10 {
25 break
26 }
27 }
28}
29
30// go思路实现
31package main
32import "fmt"
33func Count(ch chan int) {
34 ch <- 1 // 放入1
35 fmt.Println("Counting")
36}
37func main() {
38 chs := make([]chan int 10)
39 for i := 0; i < 10; i++ {
40 chs[i] = make(chan int)
41 go Count(chs[i])
42 }
43 for _, ch := range(chs) {
44 <-ch //这里是读出数据,如果数据不读取会阻塞其他协程写入数据
45 }
46}
channel,chan与map类似,没有make时是不能使用的,所以声明与make最好一起,
1var ch1 chan int
2var ch2 chan<- float64 // 单向channel, 只用于写float64数据
3var ch3 <-chan int // 单向channel, 只用于读取int数据
4ch := make(chan int) // 不带缓冲的channel,如果ch中没数据则读取会被阻塞,如果ch中有数据则写入会被阻塞
5go func(){
6 ch <- 1 // 写入数据
7}
8i := <-ch // 取数据并使用
9<- ch // 取数据但不使用, 这里将死锁
10x, ok = <- ch // ok表示有没有取到数据,与类型判断与map取值类似
11// 带缓冲的channel
12chbuf := make(chan int,100) // 没有数据时阻塞读取,数据塞满100个时阻塞写入
单向通道一般用于函数参数定义
1func single_r(ch <-chan int){
2 // do sth, 只能读取数据
3}
4func single_w(ch chan<- int){
5 // do sth, 只能写入数据
6}
7var ch chan int // 定义一个双向通道,在对应的函数内只能读或写
8single_r(ch)
9singe_w(ch)
select,与switch类似,但是是专用多通道读取
1LOOP:
2 for { // 循环读取
3 select {
4 case <-chan1:
5 // 如果chan1成功读到数据,则进行case处理语句
6 case chan2 <- 1:
7 // 如果chan2成功读到数据,则进行case处理语句
8 break LOOP // @LOOP, 退出for循环
9 default:
10 // 如果上面都没成功,则进入default处理流程
11 // 一般用 <-time.After(duration)替代default做读写默认超时处理
12 }
13 }
14
15// 读取某个chan中所有数据
16for i := range(ch){
17 // do
18}
因为select也有break语句,所以需要使用类似goto的标记来标识退出地方
通道经常使用的特殊数据:
1chan <- struct{}{}
语义为将一个空数据传递给channel.因为struct{}{}占用的内存非常小,而且我们对数据内容也不关心,通常用来做信号量来处理
七、C语言交互
将c代码用/*,*/包含起来,紧挨着写import “C"即可, 不需要特别编译处理即可直接执行
1package main
2/*
3#include <stdio.h>
4void hello() {
5 printf("Hello, Cgo! -- From C world.\n");
6}
7*/
8import "C"
9
10func Hello() {
11 C.hello()
12}
13func main() {
14 Hello()
15}
16
17// 运行结果:Hello, Cgo! -- From C world.
需要传入参数到C函数时,需要用C的类型转换后再传进去
1C.char()
2C.schar()(signed char)
3C.uchar()(unsigned char)
4C.short()
5C.ushort()(unsigned short)
6C.int()
7C.uint()(unsigned int)
8C.long()
9C.ulong()(unsigned long)
10C.longlong()(long long)
11C.ulonglong()(unsigned long long)
12C.float()
13C.double()
八、异常处理
defer, panic(), recover()异常处理,不再有烦琐的try…exception
当在一个函数执行过程中调用panic()函数时,正常的函数执行流程将立终止,但函数中之前使用defer关键字延迟执行的语句将正常展开执行,然后返回到调用函数,逐层向上执行panic,直到所属的goroutine中所有正在执行的函数被终止
recover()函数用来终止panic流程,放在defer关键词后面的函数中执行
1defer func(){
2 if err := recover(); err !=nil{
3 // 处理错误
4 }
5}
6
7// ...
8
9panic("something error") // 这个panic将被上面的defer捕获,阻止程序退出
九、学习资源
https://gobyexample.com, 这里有大量使用常规使用案例
https://play.golang.org, 线上执行环境,可演示简单程序