GO简介

Go语言(也叫 Golang)是Google开发的开源编程语言。
Logo: 囊地鼠(Gopher)
源码地址:https://github.com/golang/go

起源与发展

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 字符集规范

语言特性

Go 语法简洁,上手容易,快速编译,支持跨平台开发,自动垃圾回收机制,天生的并发特性,更好地利用大量的分布式和多核的计算机。简单来说:Go语言实现了开发效率与执行效率的完美结合,具有媲美C语言的运行速度,又具有与Python,Java语言相近开发效率。

语法特点

Go 语言支持指针,引入协程(goroutine)实现并发,通过Channel实现协程间通讯,函数方法支持多个返回值,通过 recover 和 panic 来替代异常机制,从1.18版本开始支持泛型;

没有类概念,通过结构体和interface实现面向对象,不支持函数重载,不支持隐式转换,不支持三元运算符,不支持静态变量,
不支持动态链接库和动态加载代码;

安装配置

官网下载:


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版本)
    只能该代码包得 直接父包及其子包中的代码引用
基础语法

变量

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 中被抛弃,常用于不需要从一个函数得到所有返回值

常量

常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型,值不可更改,表达式只支持内置函数

const num int = 10
const str = "Hello world" //支持按值进行类型推导
const LENGTH = len(str) //表达式只支持内置函数
//定义枚举值
const (
    Unknown = 0
    Female = 1
    Male = 2
)

基础数据类型

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

字符串

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()))
}

类型转换

基础数据类型转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"

运算符

不支持三元运算符,支持赋值交换,比如a,b=b,a

控制语句

支持 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{
}

表达式不需要(), { 不能单独放在一行, 编译报错 ,代码行之间不需要分号也不能用分号

数据结构

数组

支持多维数组,属于值类型,支持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)
}

切片

切片(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)

指针

引用类型


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

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{}实现多态,需要要继承和实现关键字

结构体

结构体是由一系列具有相同类型或不同类型的数据构成的数据集合,是值类型,通过将函数与结构体变量绑定实现方法(当然函数可以与命令类型绑定实现方法)

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!")
}

实际项目中,大部分都会绑定指针类型;既节省了资源,又同步了对象的数据

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,带来隐患和性能损耗