一、GO简介
Go语言(也叫 Golang)是Google开发的开源编程语言。
Logo: 囊地鼠(Gopher)
源码地址:https://github.com/golang/go, 代码例子:https://gobyexample.com/
1. 起源与发展
2007年末由Robert Griesemer, Rob Pike, Ken Thompson主持开发,旨在提高多核联网机器和大型代码库时代的编程效率,2009 年 11 月首次向公众发布 Go,2012年发布go稳定版。截至目前2022年,全球知名 TIOBE 编程语言社区统一语言排名第十名左右。目前主流的云原生技术很大一部分都是Go实现的,比如:Docker容器,Kubernetes,Istio,ETCD,InfluxDB等等。
- Robert Griesemer,参与开发 Java HotSpot 虚拟机;
- Rob Pike,Go 语言项目总负责人,贝尔实验室 Unix 团队成员,参与的项目包括 Plan 9,Inferno 操作系统和 Limbo 编程语言;
- Ken Thompson,贝尔实验室 Unix 团队成员,C 语言、Unix 和 Plan 9 的创始人之一,与 Rob Pike 共同开发了 UTF-8 字符集规范
2. 语言特性
Go 语法简洁,上手容易,快速编译,支持跨平台开发,自动垃圾回收机制,天生的并发特性,更好地利用大量的分布式和多核的计算机。简单来说:Go语言实现了开发效率与执行效率的完美结合,具有媲美C语言的运行速度,又具有与Python,Java语言相近开发效率。
3. 语法特点
Go 语言支持指针,引入协程(goroutine)实现并发,通过Channel实现协程间通讯,函数方法支持多个返回值,通过 recover 和 panic 来替代异常机制,从1.18版本开始支持泛型;
没有类概念,通过结构体和interface实现面向对象,不支持函数重载,不支持隐式转换,不支持三元运算符,不支持静态变量,
不支持动态链接库和动态加载代码;
二、安装配置
官网下载:
1. window开发环境
默认情况下.msi 文件会安装在 c:\Go 目录下,可选择自定义路径进行安装,设置为GOROOT的值,即Go的SDK路径。具体SDK库使用见官网标准库文档,三方中文文档:https://studygolang.com/pkgdoc
#查看版本
go version
#查看Go环境变量
go env
查看所有命令:
go help
helloworld.go 源文件如下:
package main
import "fmt"
func main() {
fmt.Println("Hello, 世界")
}
#编译 输出可执行文件.exe
go build helloworld.go
#编译并运行
go run helloworld.go
跨平台编译
#修改环境变量
SET CGO_ENABLED=0
SET GOOS=linux
#编译输出linux下可执行文件
go build helloworld.go
单元测试
命名文件时需要让文件必须以_test结尾,测试用例函数需要以Test为前缀,例如func TestXXX(t*testing.T)
helloworld_test.go 文件
import "testing"
func TestA(t *testing.T) {
t.Log("A")
}
func TestB(t *testing.T) {
t.Log("B")
}
# 执行所有测试函数
go test helloworld_test.go
# 执行指定测试函数
go test -run TestA helloworld_test.go
三、工程化
Go 官方在1.11开始推出了Go Modules的功能,配置GO111MODULE=auto 启用,1.13版本默认启动,用于go项目得依赖管理
设置代理:
go env -w GOPROXY=https://goproxy.cn
常用代理:阿里云(https://mirrors.aliyun.com/goproxy/ )七牛云(https://goproxy.cn),https://goproxy.io
#创建目录
mkdir hello
cd hello
#初始化工程
go mod init example/hello
# 下载依赖包
go get github.com/robfig/cron@v1.2.0
go get github.com/google/uuid@v1.3.0 #下载$GOPATH/pkg/mod目录下
输出:
go.mod 文件标记每个依赖包的版本
module example/hello
go 1.15
require (
github.com/google/uuid v1.3.0
github.com/robfig/cron v1.2.0
)
go.sum 文件记录每个依赖包的哈希值
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
正常情况下,每个依赖包版本会包含两条记录:
- 第一条记录为该依赖包版本整体(所有文件)的哈希值
- 第二条记录仅表示该依赖包版本中go.mod文件的哈希值
go.mod只需要记录直接依赖的依赖包版本,只在依赖包版本不包含go.mod文件时候才会记录间接依赖包版本; go.sum则是要记录构建用到的所有依赖包版本, go.sum文件中记录的依赖包版本数量往往比go.mod文件中要多
GOSUMDB 环境变量标识checksum database,用于依赖包版本写入go.sum 文件之前进行二次校验
go不允许循环依赖(包A>包B>包C>包A)
早期GOPATH工作区方式,GOPATH包含三个子目录:
- src 存放源代码的位置
- pkg 存储预编译目标文件的地方,以加速程序的后续编译
- bin 编译后的二进制文件的位置
引入三方套件,先查找目录GOPATH/src没有再查找目录GOROOT/src,如果没找到就报错了,虽然有一些第三方的包管理工具比如:Vendor、Dep,但是并不好用
引入本地包
module example/hello
go 1.15
require (
github.com/google/uuid v1.3.0
github.com/robfig/cron v1.2.0
example/greeting v1.0.0 //indirect
)
//replace 将远程包替换为本地包服 模块名=>模块路径
replace example/greeting => ../greeting
//indirect 不能省略
包管理
- 文件夹名与包名没有直接关系,并非需要一致,但建议一致
- 同一个文件夹下的文件只能有一个包名(即一个目录内只能有一个包名)
- import 两个同名包时,可以设置别名区分
import (
"example/hello/com/greeting" // 模块名+包路径
_ "example/hello/com/math" //匿名(可以不使用),促发包内init()方法
myMath "example/hello/com/math" //设置别名
"fmt"
)
func main() {
greeting.Say() //包名.方法名
fmt.Println("hello world")
}
访问控制
以首字母大小写进行变量、方法、函数得访问控制
- 首字母大写,公开的
- 首字母小写,包级私有的,只能包内访问
- internal代码包内,首字母大写的,模块级私有(go 1.4版本)
只能该代码包得 直接父包及其子包中的代码引用
四、基础语法
1.变量
var a = 100 // 支持类型推导
var b int = 200 //声明并初始化
var (
x,y,z string //只声明
)
func main() {
var a int
a = 150
var b = true
c := 300 //短变量声明并初始化,只能在函数体内使用
fmt.Println(a,b, c)
fmt.Println(x,y, z)
x = "hello world" //全局变量赋值
fmt.Println(x, y, z)
}
函数体内的变量一旦声明就一定要使用,不然会报编译出错,变量读取优先读取函数体内的,没找到再读取函数体外的
_特殊只写变量,例如 值 5 在:_, b = 5, 7 中被抛弃,常用于不需要从一个函数得到所有返回值
2.常量
常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型,值不可更改,表达式只支持内置函数
const num int = 10
const str = "Hello world" //支持按值进行类型推导
const LENGTH = len(str) //表达式只支持内置函数
//定义枚举值
const (
Unknown = 0
Female = 1
Male = 2
)
3.基础数据类型
go不支持隐式类型转换,需显示转换,要转换的类型(变量)
- 整型
有符号按长度分为:int8、int16、int32、int64 ,对应的无符号整型:uint8、uint16、uint32、uint64,默认值为0
var x int8 = 88
var y int32 = 555
y = int32(x) //低位转高位
fmt.Println(x, y) //输出88,88
var a int8 = 99
var b int32 = 9999
a = int8(b) //高位转低位 转成二进制截取
fmt.Println(a,b) //输出15,9999
特殊整型
int/uint 32位操作系统上就是int32/uint32,64位操作系统上就是int64/uint64
uintptr 无符号整型,用于存放一个指针
byte 实际是一个uint8,代表了ASCII码的一个字符
rune 实际是一个int32,代表一个UTF-8字符布尔型
true和false,默认值为false,无法参与数值运算,不允许将整型强制转换为布尔型浮点型
支持两种浮点型数:float32和float64,默认类型是float64复数
complex64和complex128
4.字符串
go语言字符串是一个任意字节的常量序列,底层是byte字节数组,len()方法计算的是字节总数,修改字符串必须先转成byte或者rune,多行字符串可以使用反引号``
func main(){
var str = "Go入门教程!"
fmt.Println(str[0:],str[0:5]) //按索引截取字符串
fmt.Println(len(str)) //输出15, 一个中文三个字节
fmt.Println(utf8.RuneCountInString(str))//输出7,代表7个字符
var charArray = []rune(str) //转成字节切片
fmt.Println(len(charArray))
//range 遍历所有字符
for _,s:= range str {
fmt.Printf("%v\t",string(s))
}
//字符串比较==
var str1= "Go入门教程" + "!"
fmt.Println(str==str1)
str += "welcome" //字符串连接
fmt.Println(str)
}
常用工具包strings
strings.ToUpper(str),strings.ToLower(str)
strings.Join([]string{"java", "go", "c++"},";")
strings.ContainsAny(str, "GW")
strings.Contains(str,"Go")
strings.Split(str,"")
strings.EqualFold(str,str1) //不区分大小比较
strings.Compare(str1,str) //区分大小写比较
字符串连接采用strings.Builder或者 bytes.Buffer,推荐strings.Builder 性能最高
func StringAppend(num int) {
var now = time.Now()
var str = ""
for i := 0; i < num; i++ {
str += "append" + strconv.Itoa(i)
}
fmt.Println(time.Since(now))
fmt.Println(len(str))
now = time.Now()
var stringBuilder = strings.Builder{}
for i := 0; i < num; i++ {
stringBuilder.WriteString("append" + strconv.Itoa(i))
}
fmt.Println(time.Since(now))
fmt.Println(len(stringBuilder.String()))
now = time.Now()
var bytebuffer bytes.Buffer
for i := 0; i < num; i++ {
bytebuffer.WriteString("append" + strconv.Itoa(i))
}
fmt.Println(time.Since(now))
fmt.Println(len(bytebuffer.String()))
}
5.类型转换
基础数据类型转string,采用strconv包 或者fmt.Sprintf()
var x int = 99
var y float32= 88.88
fmt.Sprintf("%d,%.2f",x,y)
string转基础数据类型,利用strconv包
var x = "99";
var y = "88.88"
6.运算符
不支持三元运算符,支持赋值交换,比如a,b=b,a
7.控制语句
支持 if,for,case,表达式不需要(),支持goto 语言但不推荐使用
func main() {
if x,y:=5,3; x+y > 10 { //判断表达式前支持一个赋值表达式分号分隔
fmt.Printf("%d + %d > 10",x, y)
}else if x+y < 10{
fmt.Printf("%d + %d < 10",x, y)
}else{
fmt.Printf("%d + %d = 10",x, y)
}
}
// 冒泡算法
func BubbleSort(array []int){
length := len(array)
for i:= 0; i < length-1; i++ {
for j := 0; j < length-i-1; j++{
if array[j] > array[j+1]{
array[j], array[j+1] = array[j+1],array[j]
}
}
}
}
// 等同于while
for i < 100{
}
//死循环
for{
}
表达式不需要(), { 不能单独放在一行, 编译报错 ,代码行之间不需要分号也不能用分号
五、数据结构
1.数组
支持多维数组,属于值类型,支持range遍历
例子:随机生成长度为10整数数组
func RandomArray(){
var array[10]int //声明
var max int
for i := 0; i < 10; i++{
array[i] = rand.Intn(100) //赋值 随机获取100以内的整数
if array[i] > max {
max = array[i]
}
}
fmt.Printf("数组内容:%v,长度为:%d, 最大值:%d\n",array, len(array),max) //获取数组长度len(array)
}
2.切片
切片(slice) 也叫动态数组 ,是对数组的一个连续片段的引用。底层实现是数组,是引用类型,支持range遍历
var slice = []int{10, 20, 30}
var slice1[]int
fmt.Println(slice1==nil) //true
slice1 = make([]int,5,10) //length=5,cap=10
fmt.Println(slice,slice1) //[10 20 30] [0 0 0 0 0]
//数组转切片
var array = [6]int{10, 20, 30, 40, 50, 60}
var slice = array[0:3]
fmt.Println(slice,len(slice),cap(slice)) //输出[10 20 30] 3 6
//append操作
slice1 := append(slice,99, 999)
sliceX = array[:]
i:= 3
sliceY := append(sliceX[:i],sliceY[i+1:]...) //删除sliceX[i] 元素
//copy操作 copy(dst, src []Type)
slice2 := []int{1, 2, 3}
copy(slice1, slice2) //复制slice2到slice1的前三个位置
fmt.Println(slice1) //[1 2 3 99 999]
slice3 := []int{11, 22, 33, 44, 55, 66, 77, 88, 99}
copy(slice2,slice3) //只复制slice3前三个元素到slice2的前三个位置
fmt.Println(slice2) //[11,22,33]
slice底层实现
var slice = []int{10, 20, 30, 40, 50, 60}
var newSlice = slice[1:3]
fmt.Println(len(slice),cap(slice),len(newSlice),cap(newSlice)) //6 6 2 5
newSlice[1] += 100
fmt.Println(slice) //[10 20 130 40 50 60]
newSlice = append(newSlice, 999)
fmt.Println(slice,newSlice,len(newSlice),cap(newSlice)) //[10 20 130 999 50 60] [20 130 999] 3 5
当切片Append()操作,如果容量(CAP)足够的时候,会直接在原数组上操作,共享同一个底层数据,当容量不够的时候才会进行扩容,开辟一个新的内存区域(新数组),拷贝原来值,再执行append操作。扩容策略是这样,当切片的容量小于 1024 个元素,翻倍扩容,当大于1024,每次增加原来容量的四分之一(增长因子为:1.25)
3.指针
引用类型
var a = 100
b:= &a // 取地址 赋值给指针变量
c:= *b // 指针变量中指向地址的值
var d *int //定义字符指针
d = b
fmt.Println(a,b,c,*d) //100 0xc0000120d0 100 100
*b = 666
fmt.Println(a,b,c,*d) //666 0xc0000120d0 100 666
a = 999
fmt.Println(a,b,c,*d) //999 0xc0000120d0 100 999
make 与 new, make 只能用于初始化slice,map,chan类型;new 可以用于任何类型,nil 代表空指针
var slice1 = make([]int,5)
var map1 = make(map[int]string)
var chan1 = make(chan int,3)
var x *int = new(int)
var y *int
var z * map[int]string = new(map[int]string)
fmt.Println(x==nil,y==nil,z==nil) //输出false,true,false
4.Map
引用类型,支持range遍历
var color = map[string]int{"red": 1, "blue": 2, "green": 3}
var city map[string]int // city等于nil,只声明
var red = color["red"]
var pink = color["pink"] //不存在返回对应类型的零值,这里是int类型所以是0
fmt.Println(red,pink,city==nil) //输出1,0,true
if blue,exist := color["blue"];exist{
fmt.Println("exist blue value: ",blue)
}
delete(color,"blue") //删除指定key的key-value对
六、函数
函数支持多个返回值,支持闭包函数,函数参数支持函数类型,不定长参数用 ...,不支持函数重载
- 返回多个值
func Swap(x int , y int) (int, int){
return y,x
}
- 函数参数实现回调函数
type FuncType func(x int,y int) int //声明函数类型
func Minus(x int,y int) int{
return x - y
}
func CalFun(x int,y int, cal FuncType) int{
return cal(x, y)
}
func main() {
var result= CalFun(100,200, func(x int, y int) int{ //匿名回调函数
return x + y
})
fmt.Println(result)
fmt.Println(CalFun(200,100, Minus))
}
- 递归函数实现N的阶乘
func Factorial(n int64) int64 {
if n <= 1 {
return 1
}
return n * Factorial(n-1)
}
- 闭包实现斐波那契数列
F(0)=0,F(1)=1, F(n)=F(n - 1)+F(n - 2)(n ≥ 2,n ∈ N*)
func Fibonacci() func() int{
a := 0 //代表F(n-2)
b := 1 //代表F(n-1)
var add = func() int {
result := a + b //代表F(n)
a,b =b,result
return result
}
return add
}
func main() {
var nextNum = Fibonacci() //函数变量
for i:=0; i < 10; i++ {
fmt.Print(nextNum()," ")
}
}
七、面向对象
go 语言没有类的概念,用自己的一套方式实现面向对象,通过结构体实现封装,通过结构体绑定函数实现方法,以组合的方式实现继承,(java之父也曾透露过他最想改的就是继承,觉得java继承有点重)更加解耦和轻量,利用interface{}实现多态,需要要继承和实现关键字
1.结构体
结构体是由一系列具有相同类型或不同类型的数据构成的数据集合,是值类型,通过将函数与结构体变量绑定实现方法(当然函数可以与命令类型绑定实现方法)
type Person struct {
Name string
Age uint
Sex bool //true 代表男, false 代表女
}
//直接绑定结构体(传递是值类型)
func (p Person) Say(content string){
p.Age=100 //修改拷贝的结构体的值
fmt.Println(p.Name, content)
}
//指针方式绑定结构体(传递是引用类型)
func (p *Person) SetName(name string) {
p.Name = name
}
// 采用匿名组合实现继承
type Student struct {
Person
class string
}
func TestInit(t *testing.T) {
per := Person{"张三", 34, true}
per.Say("welcome !")
per.SetName("李四")
stu:= Student{per, "一年一班"}
stu.Name = "王五"
stu.Say("hello!")
}
实际项目中,大部分都会绑定指针类型;既节省了资源,又同步了对象的数据
2.interface
即接口,它把具有共性的方法定义在一起,任何类型实现了这些方法就是实现了这个接口。
- 接口嵌套接口实现接口继承
- 接口也可以嵌入到结构中
- 指针接收者实现接口的方式只能支持赋值结构体指针给接口变量,而值接收者实现接口的方式都可以,因为Go语言中有对指针类型变量求值的语法糖
type Downloader interface {
Download()
}
type Implement interface {
download(url string)
save()
}
type Template struct {
url string
Implement
}
//模板方法
func (t Template)Download() {
fmt.Println("prepare download...")
t.download(t.url)
fmt.Println("finish download...")
t.save()
}
func (t Template)save(){
fmt.Println("save ...")
}
type HttpDownloader struct {
Template
}
func (HttpDownloader) download(url string) {
fmt.Println("http download "+url+"....")
}
func NewHttpDownloader(url string) Downloader{
downloader := &HttpDownloader{Template{url:url}}
downloader.Template.Implement = downloader
return downloader
}
type FtpDownloader struct {
Template
}
func (FtpDownloader) download(url string) {
fmt.Println("ftp download "+url+"....")
}
func NewFtpDownloader(url string) Downloader{
downloader := &FtpDownloader{Template{url:url}}
downloader.Template.Implement = downloader
return downloader
}
结构可以嵌套结构体或者接口,接口只能嵌套接口
interface{}
空接口。没有方法的接口,所有类型都至少实现了0个方法,所以所有类型都实现了空接口。interface{} 类型常用于函数参数,表示可以传递任意类型,GO运行时会执行类型转换(如果需要)。内置fmt包打印方法用到这个参数
func main() {
PrintType("string",56, &oop.Person{Name: "张三", Age: 22, Sex: true})
}
// 不定长interface{} 参数
func PrintType(v ...interface{}){
for _,valType := range v{
switch valType.(type) { //类型断言
case int:
fmt.Println("int")
case bool:
fmt.Println("bool")
case string:
fmt.Println("string")
case *oop.Person:
fmt.Println("*Person")
default:
fmt.Println("unknow")
}
}
}
八、异常处理
go 的异常处理简单轻巧高效,推荐采用将异常返回的方式代替捕获异常,可以采用panic+recover模拟类似try...catch
defer
defer语句预设延迟函数调用,无论函数怎么返回,都会执行,被延期的函数先进后出的顺序执行,常用于资源释放
func CopyFile(dstFileName string,srcFileName string) (written int64,err error) {
srcFile, err := os.Open(srcFileName)
if err != nil {
return
}
defer srcFile.Close() //声明延迟调用
//打开dstFileName
dstFile, err := os.OpenFile(dstFileName, os.O_WRONLY|os.O_CREATE, 0666) //0666 在windos下无效
if err != nil {
return
}
defer dstFile.Close() //声明延迟调用,先上面先调用
//调用copy函数
return io.Copy(dstFile,srcFile)
}
自定义异常
内嵌异常接口
type error interface {
Error() string
}
自定义异常 参考内置errors包 errorString结构体实现
func New(text string) error {
return &errorString{text}
}
// errorString is a trivial implementation of error.
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
返回错误方式例子:
func Divide(a int,b int)(float64,error){
if b==0{
return 0, errors.New("除数不能为零值")
}
return float64(a)/float64(b),nil
}
panic+recover
panic 导致程序终止运行,采用recover可以捕获panic,重新获得Go程序控制权并恢复正常运行
func Divide(a int, b int) (float64,error){
defer func() {
err := recover() //捕获到异常
if err != nil{
fmt.Println(err)
}
}()
defer fmt.Println("recover :",recover()) //recover为nil,向外传递异常
if b == 0{
panic("除数不能为零")
}
return float64(a)/float64(b),nil
}
recover只能在被延期的函数内部才有用,且只能被最后一个被延期执行的函数捕获,因为任何未捕获的错误都会沿调用堆栈向外传递,非必要不要使用panic,带来隐患和性能损耗