专注于大数据及容器云核心技术解密,可提供全栈的大数据+云原生平台咨询方案,请持续关注本套博客。QQ邮箱地址:1120746959@qq.com,如有任何学术交流,可随时联系。详情请关注《数据云技术社区》公众号。
1 Go 基本概述
1.1 概述
- 在语言层面实现了并发机制的类C通用型编程语言。
- Go关键字(25个),如上图。
- Go 1.11版本开始支持Go modules方式的依赖包管理功能
hello.go
package main
import (
"fmt"
"rsc.io/quote"
)
func main() {
fmt.Println(quote.Hello())
}
# 安装GO 1.11及以上版本
go version
# 开启module功能
export GO111MODULE=on
# 进入到项目目录
cd /home/gopath/src/hello
# 初始化
go mod init
# 编译
go build
#加载依赖包,自动归档到vendor目录
go mod vendor -v
# 文件目录结构
./
├── go.mod
├── go.sum
├── hello # 二进制文件
├── hello.go
└── vendor
├── golang.org
├── modules.txt
└── rsc.io
复制代码
- dep安装
go get -u github.com/golang/dep/cmd/dep
#进入到项目目录
cd /home/gopath/src/demo
#dep初始化,初始化配置文件Gopkg.toml
dep init
#dep加载依赖包,自动归档到vendor目录
dep ensure
# 最终会生成vendor目录,Gopkg.toml和Gopkg.lock的文件
复制代码
2 Go 基本语法
2.1 变量声明
1、单变量声明,类型放在变量名之后,可以为任意类型
var 变量名 类型
2、多变量同类型声明
var v1,v2,v3 string
3、多变量声明
var {
v1 int
v2 []int
}
4、使用关键字var,声明变量类型并赋值
var v1 int=10
5、使用关键字var,直接对变量赋值,go可以自动推导出变量类型
var v2=10
6、直接使用“:=”对变量赋值,不使用var,两者同时使用会语法冲突,推荐使用
v3:=10
7、可以限定常量类型,但非必需
const Pi float64 = 3.14
8、无类型常量和字面常量一样
const zero=0.0
9、多常量赋值
const(
size int64=1024
eof=-1
)
10、常量的多重赋值,类似变量的多重赋值
const u,v float32=0,3
const a,b,c=3,4,"foo" //无类型常量的多重赋值
11、常量赋值是编译期行为,可以赋值为一个编译期运算的常量表达式
const mask=1<<3
复制代码
2.2 变量类型
//布尔类型的关键字为bool,值为true或false,不可写为0或1
var v1 bool
v1=true
//接受表达式判断赋值,不支持自动或强制类型转换
v2:=(1==2)
//int和int32为不同类型,不会自动类型转换需要强制类型转换
//强制类型转换需注意精度损失(浮点数→整数),值溢出(大范围→小范围)
var v2 int32
v1:=64
v2=int32(v1)
//浮点型分为float32(类似C中的float),float64(类似C中的double)
var f1 float32
f1=12 //不加小数点,被推导为整型
f2:=12.0 //加小数点,被推导为float64
f1=float32(f2) //需要执行强制转换
//复数的表示
var v1 complex64
v1=3.2+12i
//v1 v2 v3 表示为同一个数
v2:=3.2+12i
v3:=complex(3.2,12)
//实部与虚部
//z=complex(x,y),通过内置函数实部x=real(z),虚部y=imag(z)
//声明与赋值
var str string
str="hello world"
//创建数组
var array1 [5]int //声明:var 变量名 类型
var array2 [5]int=[5]int{1,2,3,4,5} //初始化
array3:=[5]int{1,2,3,4,5} //直接用“:=”赋值
[3][5]int //二维数组
[3]*float //指针数组
//数组元素访问
for i,v:=range array{
//第一个返回值为数组下标,第二个为元素的值
}
//创建切片,基于数组创建
var myArray [5]int=[5]{1,2,3,4,5}
var mySlice []int=myArray[first:last]
slice1=myArray[:] //基于数组所有元素创建
slice2=myArray[:3] //基于前三个元素创建
slice3=myArray[3:] //基于第3个元素开始后的所有元素创建
//直接创建
slice1:=make([]int,5) //元素初始值为0,初始个数为5
slice2:=make([]int,5,10) //元素初始值为0,初始个数为5,预留个数为10
slice3:=[]int{1,2,3,4,5} //初始化赋值
//基于切片创建
oldSlice:=[]int{1,2,3,4,5}
newSlice:=oldSlice[:3] //基于切片创建,不能超过原切片的存储空间(cap函数的值)
//动态增减元素,切片分存储空间(cap)和元素个数(len),当存储空间小于实际的元素个数,会重新分配一块原空间2倍的内存块,并将原数据复制到该内存块中,合理的分配存储空间可以以空间换时间,降低系统开销。
//添加元素
newSlice:=append(oldSlice,1,2,3) //直接将元素加进去,若存储空间不够会按上述方式扩容。
newSlice1:=append(oldSlice1,oldSlice2...) //将oldSlice2的元素打散后加到oldSlice1中,三个点不可省略。
//内容复制,copy()函数可以复制切片,如果切片大小不一样,按较小的切片元素个数进行复制
slice1:=[]int{1,2,3,4,5}
slice2:=[]int{6,7,8}
copy(slice2,slice1) //只会复制slice1的前三个元素到slice2中
copy(slice1,slice1) //只会复制slice2的三个元素到slice1中的前三个位置
//map先声明后创建再赋值
var map1 map[键类型] 值类型
//创建
map1=make(map[键类型] 值类型)
map1=make(map[键类型] 值类型 存储空间)
//赋值
map1[key]=value
// 直接创建
m2 := make(map[string]string)
// 然后赋值
m2["a"] = "aa"
m2["b"] = "bb"
// 初始化 + 赋值一体化
m3 := map[string]string{
"a": "aa",
"b": "bb",
}
//delete()函数删除对应key的键值对,如果key不存在,不会报错;如果value为nil,则会抛出异常(panic)。
delete(map1,key)
//元素查找
value,ok:=myMap[key]
if ok{
//如果找到
//处理找到的value值
}
//遍历
for key,value:=range myMap{
//处理key或value
}
复制代码
2.3 流程管理
- 条件语句
//在if之后条件语句之前可以添加变量初始化语句,用;号隔离
if <条件语句> { //条件语句不需要用括号括起来,花括号必须存在
//语句体
}else{
//语句体
}
//在有返回值的函数中,不允许将最后的return语句放在if...else...的结构中,否则会编译失败
//例如以下为错误范例
func example(x int) int{
if x==0{
return 5
}else{
return x //最后的return语句放在if-else结构中,所以编译失败
}
}
复制代码
- 选择语句
//1、根据条件不同,对应不同的执行体
switch i{
case 0:
fmt.Printf("0")
case 1: //满足条件就会退出,只有添加fallthrough才会继续执行下一个case语句
fmt.Prinntf("1")
case 2,3,1: //单个case可以出现多个选项
fmt.Printf("2,3,1")
default: //当都不满足以上条件时,执行default语句
fmt.Printf("Default")
}
//2、该模式等价于多个if-else的功能
switch {
case <条件表达式1>:
语句体1
case <条件表达式2>:
语句体2
}
复制代码
- 循环语句
//1、Go只支持for关键字,不支持while,do-while结构
for i,j:=0,1;i<10;i++{ //支持多个赋值
//语句体
}
//2、无限循环
sum:=1
for{ //不接条件表达式表示无限循环
sum++
if sum > 100{
break //满足条件跳出循环
}
}
//3、支持continue和break,break可以指定中断哪个循环,break JLoop(标签)
for j:=0;j<5;j++{
for i:=0;i<10;i++{
if i>5{
break JLoop //终止JLoop标签处的外层循环
}
fmt.Println(i)
}
JLoop: //标签处
...
复制代码
- 跳转语句
//关键字goto支持跳转
func myfunc(){
i:=0
HERE: //定义标签处
fmt.Println(i)
i++
if i<10{
goto HERE //跳转到标签处
}
}
复制代码
- 函数定义与调用
//1、函数组成:关键字func ,函数名,参数列表,返回值,函数体,返回语句
//先名称后类型
func 函数名(参数列表)(返回值列表){ //参数列表和返回值列表以变量声明的形式,如果单返回值可以直接加类型
函数体
return //返回语句
}
//例子
func Add(a,b int)(ret int,err error){
//函数体
return //return语句
}
//2、函数调用
//先导入函数所在的包,直接调用函数
import "mymath"
sum,err:=mymath.Add(1,2) //多返回值和错误处理机制
复制代码
- 多返回值
//多返回值
func (file *File) Read(b []byte) (n int,err error)
//使用下划线"_"来丢弃返回值
n,_:=f.Read(buf)
复制代码
- 匿名函数
//匿名函数:不带函数名的函数,可以像变量一样被传递。
func(a,b int,z float32) bool{ //没有函数名
return a*b<int(z)
}
f:=func(x,y int) int{
return x+y
}
复制代码
3 对象编程
3.1 对象(属性进行定义,不含方法)
- struct实际上就是一种复合类型,只是对类中的属性进行定义赋值,并没有对方法进行定义,方法可以随时定义绑定到该类的对象上,更具灵活性。可利用嵌套组合来实现类似继承的功能避免代码重复。
type Rect struct{ //定义矩形类
x,y float64 //类型只包含属性,并没有方法
width,height float64
}
func (r *Rect) Area() float64{ //为Rect类型绑定Area的方法,*Rect为指针引用可以修改传入参数的值
return r.width*r.height //方法归属于类型,不归属于具体的对象,声明该类型的对象即可调用该类型的方法
}
复制代码
3.2 方法(附属到对象)
- 方法:为类型添加方法,方法即为有接收者的函数 func (对象名 对象类型) 函数名(参数列表) (返回值列表), 可随时为某个对象添加方法即为某个方法添加归属对象(receiver)
type Integer int
func (a Integer) Less(b Integer) bool{ //表示a这个对象定义了Less这个方法,a可以为任意类型
return a<b
}
//类型基于值传递,如果要修改值需要传递指针
func (a *Integer) Add(b Integer){
*a+=b //通过指针传递来改变值
}
复制代码
3.3 初始化[实例化对象]
new()
func new(Type) *Type
内置函数 new 分配空间。传递给new 函数的是一个类型,不是一个值。返回值是指向这个新分配的零值的指针
//创建实例
rect1:=new(Rect) //new一个对象
rect2:=&Rect{} //为赋值默认值,bool默认值为false,int默认为零值0,string默认为空字符串
rect3:=&Rect{0,0,100,200} //取地址并赋值,按声明的变量顺序依次赋值
rect4:=&Rect{width:100,height:200} //按变量名赋值不按顺序赋值
//构造函数:没有构造参数的概念,通常由全局的创建函数NewXXX来实现构造函数的功能
func NewRect(x,y,width,height float64) *Rect{
return &Rect{x,y,width,height} //利用指针来改变传入参数的值达到类似构造参数的效果
}
//方法的重载,Go不支持方法的重载(函数同名,参数不同)
//v …interface{}表示参数不定的意思,其中v是slice类型,及声明不定参数,可以传入任意参数,实现类似方法的重载
func (poem *Poem) recite(v ...interface{}) {
fmt.Println(v)
}
复制代码
3.4 匿名组合[继承]
- 组合,即方法代理,例如A包含B,即A通过消息传递的形式代理了B的方法,而不需要重复写B的方法。
func (base *Base) Foo(){...} //Base的Foo()方法
func (base *Base) Bar(){...} //Base的Bar()方法
type Foo struct{
Base //通过组合的方式声明了基类,即继承了基类
...
}
func (foo *Foo) Bar(){
foo.Base.Bar() //并改写了基类的方法,该方法实现时先调用基类的Bar()方法
... //如果没有改写即为继承,调用foo.Foo()和调用foo.Base.Foo()的作用的一样的
}
//修改内存布局
type Foo struct{
... //其他成员信息
Base
}
//以指针方式组合
type Foo struct{
*Base //以指针方式派生,创建Foo实例时,需要外部提供一个Base类实例的指针
...
}
//名字冲突问题,组合内外如果出现名字重复问题,只会访问到最外层,内层会被隐藏,不会报错,即类似java中方法覆盖/重写。
type X struct{
Name string
}
type Y struct{
X //Y.X.Name会被隐藏,内层会被隐藏
Name string //只会访问到Y.Name,只会调用外层属性
}
复制代码
3.5 可见性[封装]
- 封装的本质或目的其实程序对信息(数据)的控制力。封装分为两部分:该隐藏的隐藏,该暴露的暴露。封装可以隐藏实现细节,使得代码模块化。
type Rect struct{
X,Y float64
Width,Height float64 //字母大写开头表示该属性可以由包外访问到
}
func (r *Rect) area() float64{ //字母小写开头表示该方法只能包内调用
return r.Width*r.Height
}
复制代码
3.6 接口[多态]
- Go语言的接口是隐式存在,只要实现了该接口的所有函数则代表已经实现了该接口,并不需要显式的接口声明。
- Go语言非侵入式接口:一个类只需要实现了接口要求的所有函数就表示实现了该接口,并不需要显式声明
type File struct{
//类的属性
}
//File类的方法
func (f *File) Read(buf []byte) (n int,err error)
func (f *File) Write(buf []byte) (n int,err error)
func (f *File) Seek(off int64,whence int) (pos int64,err error)
func (f *File) Close() error
//接口1:IFile
type IFile interface{
Read(buf []byte) (n int,err error)
Write(buf []byte) (n int,err error)
Seek(off int64,whence int) (pos int64,err error)
Close() error
}
//接口2:IReader
type IReader interface{
Read(buf []byte) (n int,err error)
}
//接口赋值,File类实现了IFile和IReader接口,即接口所包含的所有方法
var file1 IFile = new(File)
var file2 IReader = new(File)
复制代码
- 只要类实现了该接口的所有方法,即可将该类赋值给这个接口,接口主要用于多态化方法。即对接口定义的方法,不同的实现方式。
//接口animal
type Animal interface {
Speak() string
}
//Dog类实现animal接口
type Dog struct {
}
func (d Dog) Speak() string {
return "Woof!"
}
//Cat类实现animal接口
type Cat struct {
}
func (c Cat) Speak() string {
return "Meow!"
}
//Llama实现animal接口
type Llama struct {
}
func (l Llama) Speak() string {
return "?????"
}
//JavaProgrammer实现animal接口
type JavaProgrammer struct {
}
func (j JavaProgrammer) Speak() string {
return "Design patterns!"
}
//主函数
func main() {
animals := []Animal{Dog{}, Cat{}, Llama{}, JavaProgrammer{}} //利用接口实现多态
for _, animal := range animals {
fmt.Println(animal.Speak()) //打印不同实现该接口的类的方法返回值
}
}
复制代码
4 Goroutine机制
- 执行体是个抽象的概念,在操作系统中分为三个级别:进程(process),进程内的线程(thread),进程内的协程(coroutine,轻量级线程)。
- 协程的数量级可达到上百万个,进程和线程的数量级最多不超过一万个。
- Go语言中的协程叫goroutine,Go标准库提供的调用操作,IO操作都会出让CPU给其他goroutine,让协程间的切换管理不依赖系统的线程和进程,不依赖CPU的核心数量。
- 并发编程的难度在于协调,协调需要通过通信,并发通信模型分为共享数据和消息.
- 共享数据即多个并发单元保持对同一个数据的引用,数据可以是内存数据块,磁盘文件,网络数据等。数据共享通过加锁的方式来避免死锁和资源竞争.
- Go语言则采取消息机制来通信,每个并发单元是独立的个体,有独立的变量,不同并发单元间这些变量不共享,每个并发单元的输入输出只通过消息的方式。
//定义调用体
func Add(x,y int){
z:=x+y
fmt.Println(z)
}
//go关键字执行调用,即会产生一个goroutine并发执行
//当函数返回时,goroutine自动结束,如果有返回值,返回值会自动被丢弃
go Add(1,1)
//并发执行
func main(){
for i:=0;i<10;i++{//主函数启动了10个goroutine,然后返回,程序退出,并不会等待其他goroutine结束
go Add(i,i) //所以需要通过channel通信来保证其他goroutine可以顺利执行
}
}
复制代码
- channel就像管道的形式,是goroutine之间的通信方式,是进程内的通信方式,跨进程通信建议用分布式系统的方法来解决,例如Socket或http等通信协议。channel是类型相关,即一个channel只能传递一种类型的值,在声明时指定。
//1、channel声明,声明一个管道chanName,该管道可以传递的类型是ElementType
//管道是一种复合类型,[chan ElementType],表示可以传递ElementType类型的管道[类似定语从句的修饰方法]
var chanName chan ElementType
var ch chan int //声明一个可以传递int类型的管道
var m map[string] chan bool //声明一个map,值的类型为可以传递bool类型的管道
复制代码
- 缓冲机制:为管道指定空间长度,达到类似消息队列的效果
//缓冲机制
c:=make(chan int,1024) //第二个参数为缓冲区大小,与切片的空间大小类似
//通过range关键字来实现依次读取管道的数据,与数组或切片的range使用方法类似
for i :=range c{
fmt.Println("Received:",i)
}
//超时机制:利用select只要一个case满足,程序就继续执行而不考虑其他case的情况的特性实现超时机制
timeout:=make(chan bool,1) //设置一个超时管道
go func(){
time.Sleep(1e9) //设置超时时间,等待一分钟
timeout<-true //一分钟后往管道放一个true的值
}()
//
select {
case <-ch: //如果读到数据,则会结束select过程
//从ch中读取数据
case <-timeout: //如果前面的case没有调用到,必定会读到true值,结束select,避免永久等待
//一直没有从ch中读取到数据,但从timeout中读取到了数据
}
复制代码
- 管道读写
//管道写入,把值想象成一个球,"<-"的方向,表示球的流向,ch即为管道
//写入时,当管道已满(管道有缓冲长度)则会导致程序堵塞,直到有goroutine从中读取出值
ch <- value
//管道读取,"<-"表示从管道把球倒出来赋值给一个变量
//当管道为空,读取数据会导致程序阻塞,直到有goroutine写入值
value:= <-ch
复制代码
- select机制
//每个case必须是一个IO操作,面向channel的操作,只执行其中的一个case操作,一旦满足则结束select过程
//面向channel的操作无非三种情况:成功读出;成功写入;即没有读出也没有写入
select{
case <-chan1:
//如果chan1读到数据,则进行该case处理语句
case chan2<-1:
//如果成功向chan2写入数据,则进入该case处理语句
default:
//如果上面都没有成功,则进入default处理流程
}
复制代码
5 Goroutine调度
- M:machine,代表系统内核进程,用来执行G。(工人)
- P:processor,代表调度执行的上下文(context),维护了一个本地的goroutine的队列。(小推车)
- G:goroutine,代表goroutine,即执行的goroutine的数据结构及栈等。(砖头)
5.1 调度本质
- 调度的本质是将G尽量均匀合理地安排给M来执行,其中P的作用就是来实现合理安排逻辑。
5.2 抢占式调度(阻塞)
- 当goroutine发生阻塞的时候,可以通过P将剩余的G切换给新的M来执行,而不会导致剩余的G无法执行,如果没有M则创建M来匹配P。
5.3 偷任务
P可以偷任务(即goroutine),当某个P的本地G执行完,且全局没有G需要执行的时候,P可以去偷别的P还没有执行完的一半的G来给M执行,提高了G的执行效率。
6 总结
专注于大数据及容器云核心技术解密,可提供全栈的大数据+云原生平台咨询方案,请持续关注本套博客。QQ邮箱地址:1120746959@qq.com,如有任何学术交流,可随时联系。详情请关注《数据云技术社区》公众号。
有疑问加站长微信联系(非本文作者)