一、背景
1.学习方向
- 区块链研发工程师
- 分布式,去中心化
- Go服务器端/游戏软件工程师
- C和C++层能做的Go也能做,例如处理日志,数据打包,虚拟机处理明文件系统等
- 利于数据处理,高并发等
- Golang分布式/云计算软件工程师
- 云计算的底层结构组件是Golang(k8s istio微服务 prometheus等)
- 大数据开发工程师
2.应用领域
- 区块链技术,简称BT技术,也被称之为分布式账本技术
- 后端服务器应用
- 美团后台流量(排序,推荐,搜索等),提供负载均衡,Cache,容错,按条件分流,统计运行指标(qps,latency)等功能
- 数据处理
- 云计算和云服务
- golang的计算能力强
- golang的计算能力强
3.为什么创建Go语言
- 计算机硬件技术更新频繁,性能提高很快,目前主流的编程语言发展明显落后于硬件,不能合理利用多核多CPU的优势来提升软件系统性能
- 软件系统复杂度越来越高,维护成本越来越高,目前缺乏一个足够简洁高效的编程语言【现有的语言:风格不统一,计算能力不够,处理大并发不够好】
- 企业运行维护很多C/C++的项目,C/C++程序运行速度虽然很快,但是编译速度却很慢,同时还存在内存泄露的一系列的困扰需要解决
4.Go语言的特点
- Go语言保证了既能到达静态变易语言的安全和性能,又达到了动态语言开发和维护的高效率,使用一个表达式来形容,Go = C + Python,说明Go语言既有C静态语言程序的运行速度,又能到达Python动态语言的快速开发
- 从C中继承了很多理念,包括表达式语法,控制结构,基础数据类型等,也保留了和C语言一样的编译执行方式及弱化的指针
- 引入包的概念,用于组织程序结构,Go语言的一个文件都要归属于一个包,而不能单独存在
- 垃圾回收机制,内存自动回收,不需要开发人员管理
- 天然并发
- 从语言层面支持并发,实现简单
- goroutine,轻量级线程,可实现大并发处理,高效利用多核
- 基于CPS并发模型实现
- 吸收了管道通信机制,形成Go语言特有的管道channel,通过管道Channel可以实现不同的goroute之间的相互通信
- 函数可以返回多个值
- 新创新:比如切片,延时执行defer等
- 相比于C/C++的语言
- 程序编译时间短
- 像动态语言一样灵活
- 内置并发支持
二、基本语法
1.注意事项
- 以.go为后缀名
- 不需要加;
- 严格区分大小写
- ++和–只能独立使用不能赋值,且只能写在变量后面
- Go中定义的变量或者import包没有用到,代码不能编译通过
2.规范的代码风格
- 尽量使用行注释
- 可以利用gofmt来进行格式化(或者goland保存自动格式化)
- 运算符两边习惯性各加一个空格
3.标识符规则
- 可以用中文或其他UNICODE字符来定义
- 不能以数字开头,不能有其他特殊符号,不能用关键字
- 以大写字母或_开头的为导出标志符,可以理解为public,可以被其他包访问
- 其他的为非导出标志符,可以理解为private
- 变量名、函数名、常量名:采用驼峰法
- 包名:保持package的名字和目录保持一致,尽量采用有意义的包名,简短,有意义,不要和标准库冲突
4.执行流程
说明:两种执行流程方式的区别(go run和go build)
- 如果先编译生成了可执行文件,那么可以将该可执行文件拷贝到没有go开发环境的机器上
- 如果直接go run go源代码,如果要在另一台机器上这么运行,也需要go开发环境,否则无法执行
- 在编译时,编译器会将程序运行依赖的库文件包含在可执行文件中,所有,可执行文件变大了很多
5.注释方法
- 单行注释://
- 多行注释:/* */
6.转义字符
\t | 制表位,实现对齐功能,可用于排版 |
---|---|
\n | 换行符 |
\\ | 一个\ |
" | 一个“ |
\r | 一个回车 |
7.多重赋值
Go可以进行多重赋值,在交换时就无须创建临时变量了
// 交换两个数
a, b = b, a
8.流程控制
1.if-else
- 即使只有一行代码,也要有大括号
// 特殊语法
// 可以在if语句中直接定义变量
if age := 20; age > 18 {
...
}
// 一般语法
if age > 18 {
} else if age == 18 {
} else {
}
2.for
for i:=0; i < 10; i++ {
fmt.Println(i)
}
for {
break; // golang中没有while关键字,可以用for+break代替实现
}
for true {
...
}
3.switch-case
// 第一种
switch n := rand.Intn(100); n%9 {
case 0:
fmt.Println()
case 1,2,3:
fmt.Println()
fallthrough // 该关键字可以让这个分支跳入下一个分支,而不是直接break
default:
}
// 第二种
var a = "hello"
switch a {
case "hello":
fmt.Println(1)
case "world":
fmt.Println(2)
default:
fmt.Println(0)
}
4.goto
- goto可以无条件地转移到程序中指定的行
- goto语句通常与条件语句配合使用,可用来实现条件转移,跳出循环体等功能
- 在Go程序设计中一般不主张使用goto语句,以便造成程序流程的混乱
goto label
label:statement
三、数据类型
1.基本数据类型
1.布尔类型
bool
- 不能用0或非0的整数代替true和false
2.整数类型
int8、uint8(byte)、int16、uint16、int32(rune)、uint32、int64、uint64、int、uint和uintptr
- Go中没有专门的字符类型,如果要存储单个字符,一般用byte来保存
3.浮点数类型
float32(单精度)、float64(双精度)
4.复数类型
complex64、complex128
5.字符串类型
- 统一使用UTF-8编码
- 字符串一旦赋值不可变
- 可以使用双引号"",也可以使用反引号`
- 反引号可以以字符串的原生形式输出,包括换行和特殊字符,无需使用转义字符,可以实现防止攻击
- 拼接字符串
str1 += str2
str1 = str1 + str2
- 获取字符串长度
len(str)
- 获取字节和子串
// 1.获取字节
str[i]
// 2.获取子串
str[start:end]
- 转换字节切片
var str string = "world
s := []byte(str)
- 比较字符串
// 一次比较字节,两个相等的字符串的比较的时间复杂度取决于它们底层引用的字符串切片的指针是否相等
str1 == str2
6.基本数据类型之间的转换
- 不能自动转换类型,需要显示转换(低精度转高精度也需要显示声明)
var i = 100
var n1 = float32(i)
- 基本数据类型转string相互转换
// 1.fmt.Sprintf()格式化转换
var n = 99
str = fmt.Sprintf("%d", n)
fmt.Printf("str type %T str = %v\n", str, str)
// 2.使用strconv包的函数
var n = 99
str = strconv.FormatInt(n, 10)
- string转基本数据类型
var str = "true"
var b bool
b, _ = strconv.ParseBool(str)
var n = "99"
var c int
c , _ = strconv.ParseInt(n)
2.组合类型
1.指针
1.基本介绍
- 基本数据类型,变量存的就是值,也叫值类型
- 获取变量的地址,用&,比如:var num int,获取num的地址:&num
- 指针类型,变量存的是一个地址,这个地址指向的空间存的才是值,比如:var ptr *int = &num
// 1.ptr是一个指针变量
// 2.ptr的类型是*int
// 3.ptr本身的值&i
var i int = 10
var ptr *int = &i
- 获取指针类型所指向的值,使用:*,比如:var ptr *int,使用*ptr获取ptr指向的值
- 值类型,都有对应的指针类型,形式为 *数据类型
- 值类型包括
- 2.通过指针修改值
var num int = 9
var ptr *int = &num
*ptr = 10 // 修改值,会改变num的值
fmt.Println("num =", num)
2.为什么需要指针
func double(x int){
x += x // 值传递
}
func double(x *int){
*x += *x // 引用传递
}
3.go中的两种指针类型
- 类型安全的
- 非类型安全的
unsafe.Pointer
2.结构体
1.定义
// 定义
struct {
title,author string // 多定义
pages int // 单定义
}
// 使用
package main
import (
"fmt"
)
func main() {
book := Book{"Go语言101", "老貘", 256}
fmt.Println(book) // {Go语言101 老貘 256}
// 使用带字段名的组合字面量来表示结构体值。
book = Book{author: "老貘", pages: 256, title: "Go语言101"} // 在同一行,最后一个逗号可以省略,否则不能
// title和author字段的值都为空字符串"",pages字段的值为0。
book = Book{}
// title字段空字符串"",pages字段为0。
book = Book{author: "老貘"}
// 使用选择器来访问和修改字段值。
var book2 Book // <=> book2 := Book{}
book2.author = "Tapir"
book2.pages = 300
fmt.Println(book2.pages) // 300
}
2.特点
- 可以寻址
var struct_pointer *Books // 结构体寻址
book1 := &Book{100} // 结构体元素寻址
- 指针包裹类型:含有一个指针字段(直接或间接)的结构体
- 指针持有者类型:一个含有(直接或间接)指针的类型
3.类型内嵌
结构体可以包含一个或多个匿名字段,此字段可以是一个结构体类型
type inner struct {
in1,in2 int
string
}
type outer struct {
int
inner // 内嵌结构体,相当于继承
}
func main() {
instance := new(outer)
instance.int = 1 // 可以用这种方式来改变匿名变量
instance.in1 = 2 // 可以直接访问内嵌结构体的成员变量
instance.in2 = 3
ans := outer{1,inner{1,2,"name"}} // 也可以用这种方式来访问结构体
}
3.函数
- 在Go中,函数也可以当做是一种类型,函数类型的值称为函数值,默认值为nil(内置函数和init不可被用作函数值)
func Double(n int) int {
return n + n
}
func main() {
var f func(n int) int // 默认值为nil
fmt.Println(f(9)) // 直接调用,返回值为12
}
1.声明函数
func Squresss(a int, b int)(s int, d int) {
x, y := a + b, a - b
s = x * x // 无需声明
d = y * y
return // <=> return s, d
}
// 函数原型(不带方法体)
func Squresss(a int, b int)(int, int) // 可以匿名
func Squresss(a int, b int)(s,d int) // 可以公用一个类型
func Squressa(a int) int // 只有一个返回值的情况
func Squressa(int, int) (int, int) // 标准函数字面形式
// 声明边长函数
func Sum(tmp int, value ...int)(sum int) // 最后一个参数必须为变长参数
b0 := Sum([]int{2, 3, 4}...) // 注意用切片传参时必须加...
- 第一部分是输入参数列表,第二部分是返回值列表,可以有多个返回值
- 函数内不能定义函数
2.匿名函数
Go支持匿名函数
package main
func main() {
// 这个匿名函数没有输入参数,但有两个返回结果。
x, y := func() (int, int) {
println("This function has no parameters.")
return 3, 4
}() // 一对小括号表示立即调用此函数。不需传递实参。
// 下面这些匿名函数没有返回结果。
func(a, b int) {
println("a*a + b*b =", a*a + b*b) // a*a + b*b = 25
}(x, y) // 立即调用并传递两个实参。
func(x int) {
// 形参x遮挡了外层声明的变量x。
println("x*x + y*y =", x*x + y*y) // x*x + y*y = 32
}(y) // 将实参y传递给形参x。
func() {
println("x*x + y*y =", x*x + y*y) // x*x + y*y = 25
}() // 不需传递实参。
f := func(n int) bool { // 匿名函数赋值给一个函数类型的变量,并在后续多次调用
return n * n
}
}
3.内置函数
4.终止条件
// 如果一个函数有返回值,则它的函数体内最后一条语句必须为一条终止语句,但不一定要是return
func fa() int {
a:
goto a
}
func fb() bool {
for{}
}
4.容器
分为三种:数组,切片和映射。每个容器用来表示和存储一个元素序列或集合,一个容器中所有元素的类型是相同的
- 存储在容器中的每个元素都关联着一个key
- 所有容器均是不安全的
1.通用方法
- 获取容器长度
len()
- 获取容量
cap()
- 添加元素
m[k] = e
- 复制元素
m := copy(dest, src) // 第一个为要复制的切片,第二个为源切片,m表示复制了多少个元素, 为两个切片的长度的较小值
- 遍历数组
- 如果容器是一个数组,则会创建它的一个副本,对其进行的操作不会改变原容器
- 如果是一个切片或映射,则对其操作会改变原容器
// 第一种
for key, element = range aContainer {
}
// 第二种
for key = range aContainer { // 舍弃了元素,效率更高
element = aContainer[key]
}
2.数组
- 数组中的所有元素为对应元素类型的默认值
- 数组长度和容量固定
- 数组长度等于容量
- 对于一个大尺寸的数组,复制一个指针或切片比直接复制一个数组效率高得多
- 定义数组
s0 := [5]string // 定义数组长度
[]bool{false, true, true, false}
s1 := [...]int{1,2,1} // 根据元素数量自动推断数组长度
s1 := s0[1:3] // 子切片
a := [100]int
for i,n := range &a {
}
for i, n := range a[:] {
}
3.切片
- 元素默认值为nil
- 切片值的长度和容量可在运行时改变
- 切片的容量大于切片的长度
- 切片是可寻址的
s0 := []book
[]*int // 元素类型为一个指针类型:*int
[]string{0: "break", 1: "continue", 2:"fallthrough"} // 按索引取值
// 利用make创建
make([]int, 3, 5) // 第一个表示长度,第二个表示容量
s1 := s0[1:3] // 子切片
var m []int
m == nil // 判断是否为空
// 扩展操作
s1 := append(s0,7) // append为变长参数函数
4.映射
- 映射的键值类型可以使任何可比较类型
- 大多数元素为0的情况下,使用映射可以节省大量的内存
- 无序
- 元素默认值为nil
- 映射元素不可寻址
// 利用make来创建
m := make(map[bool]int)
m := make(map[bool][]string) // 元素类型为一个切片类型
m := make(map[bool][6]int) // 元素类型为一个数组类型
m := make(map[string]int{"C": 1972, "Python": 1991, "Go": 2009})
// 初始化 + 赋值一体化
m3 := map[string]string{
"a": "aa",
"b": "bb",
}
// 删除元素
delete(m,key)
//判断某个键是否存在map中
value, ok := a["hello"] //如果存在,value的值就为该键对应的值(否则value就为0),ok就为true(否则为false)
// 判断是否为空
m == nil
5.接口
1.特点
- 接口可以用来包裹非接口值
- 接口的特点是高内聚低耦合
- 通过值包裹,反射和多态得以实现
- 接口可用作泛型的类型约束
- Go中接口的实现是隐式的,没有Implements关键字,所谓的实现接口,指的是实现接口声明的所有方法
- 接口中不能包含任何变量
- Go中的接口是基于方法的,如果多个接口实现了同一个方法,则相当于实现了多个接口
2.方法元素
- 一个方法元素呈现为一个方法描述
3.类型元素
- 可以是一个类型名称、一个类型字面表示形式、一个近似类型或者一个类型并集
4.实现接口
// 声明接口
type USB interface {
Start()
Stop()
}
type Phone struct {
}
// 让Phone实现USB接口的
func (p Phone) Start() {
}
func (p Phone) Stop() {
}
// 或
func (p Phone) Working(usb USB) {
usb.Start()
usb.Stop()
}
5.注意事项
- 接口本身不能创建实例,但是可以指向一个实现了该接口的自定义类型的变量(多态)
- 只要是自定义数据类型,就可以实现接口,不仅仅是结构体类型
- interface类型默认是一个指针**(引用类型)**如果没有对interface初始化就是用,那么会输出nil
- 空接口interface{}没有任何方法,所以所有类型都实现了空接口。即可以把任何变量赋值给空接口
- 如果是指针实现的方法,则赋值的时候也需要加指针
6.应用
1.重写排序方法
7.接口体现多态特征
- 多态参数
- 接口可以接受多个实现接口的变量
- 多态数组
- 在一个接口数组中,可以存放不同的实现了接口的结构体
package main
import "fmt"
type USB interface {
Start()
}
type Phone struct {
name string
}
type Camera struct {
name string
}
// 让Phone实现USB接口的
func (p Phone) Start() {
}
func (cam Camera) Start() {
}
func main() {
var usb [3]USB
usb[0] = Phone{"iPhone"}
usb[1] = Phone{"HW"}
usb[2] = Camera{"Sony"}
fmt.Println(usb)
}
8.类型断言
类型断言(Type Assertion)是一个使用在接口值上的操作,用于检查接口类型变量所持有的的值是否实现了期望的接口或者具体的类型
value,ok := x.(T) // x,表示一个接口类型,T为一个具体类型或接口
- 该断言表达式会返回x的值和一个布尔值
- 如果T是具体某个类型,类型断言会检查x的动态类型是否等于具体类型T。如果检查成功,类型断言返回的结果是x的动态值,其类型是T
- 如果T是接口类型,类型断言会检查x的动态类型是否满足T。如果检查成功,x的动态值不会被提取,返回值是一个类型为T的接口值
- 无论T是什么类型,如果x是nil接口值,类型断言都会失败
func main() {
var x interface{} // 空接口可以接收任意类型
x = 10
value, ok := x.(int) // 输出10,true
}
- 应用
import (
"fmt"
)
func TypeJudge(items ...interface{}) {
for i, item := range items {
switch x := item.(type) {
case bool:
fmt.Printf("第%d个参数是bool类型,值是%v\n", i+1, x)
case float32, float64:
fmt.Printf("第%d个参数是float类型,值是%v\n", i+1, x)
case int, int8, int16, int32, int64:
fmt.Printf("第%d个参数是int类型,值是%v\n", i+1, x)
case nil:
fmt.Printf("第%d个参数是nil,值是%v\n", i+1, x)
case string:
fmt.Printf("第%d个参数是string类型,值是%v\n", i+1, x)
default:
fmt.Printf("第%d个参数是unknow,值是%v\n", i+1, x)
}
}
}
func main() {
var a float32 = 10.25
var b float64 = 50.41
var flag bool = true
var name string = "小明"
var c int = 20
var d int8 = 21
TypeJudge(nil, a, b, flag, name, c, d)
}
6.通道(channel)
1.特点
- 通过共享内存来通讯和通过通讯来共享内存是并发编程的两种编程风格。通过共享内存来通讯时,需要传统的并发同步技术(比如互斥锁)来避免数据竞争
- 通道的本质就是一个队列
- 通道是线程安全的,多goroutine访问时,不需要加锁,不会发生资源竞争问题
- channel是有类型的,一个string的channel只能存放string类型数据
- channel是引用类型,其本身也有地址
- 在没有使用协程的情况下,如果channel数据取完了,再取,就会报dead lock
2.定义
- 通道默认为双向的,但也可以定义其只能发送或只能接受
// 1.定义
var Chan chan int // 定义双向通道
var Chan chan<-int // 声明为只能发送
var Chan <- chan int // 声明为只能接收
var mapChan chan map[int]string
var perChan chan *Person
var allChan chan interface{} // 可存放任意数据类型的通道
// 2.初始化
// channel必须初始化才能写入数据,即make后才能使用
intChan = make(chan int, 3) // 初始化缓冲通道
intChan2 = make(chan int) // 定义非缓冲通道
3.方法
- 向通道写入数据
intChan <- 10
- 从通道中接收数据
<- intChan
num :=<-intChan // 遵循FIFO原则
- 获取通道的容量和长度
cap(intChan) // 注意当添加数据超过容量会报错
len(intChan)
- 关闭通道
// 使用内置函数close可以关闭channel,当channel关闭后,不能再向channel写数据,但是可以从channel中读取数据
close(intChan)
x,ok := <-Chan // 判断通道是否关闭,true表示通关打开,false表示通道关闭
- 遍历操作
// channel支持for-range遍历
// 在遍历时,需要关闭channel,如果channel没有关闭,会出现dead locK错误
for i := range intChan {
fmt.Print(i)
}
// 相当于
for {
v, ok = <-aChannel
if !ok {
break
}
// 使用v
}
4.通道的阻塞
- 如果编译器运行发现一个通道只有写,没有读,则该通道会阻塞
- 写通道和读通道的频率不一致没关系
- 不确定什么时候关闭通道时,可以使用select解决从通道取数据的阻塞问题
5.select语句
- go select是一种仅能用于channel发送和接收消息的语句,此语句运行期间是阻塞的;当 select中没有case语句的时候,会阻塞当前goroutine
- select是go在语言层面提供的IO多路复用机制,专门用于检测多个channel是否准备完毕:可读可写
- select语句中除default外,每个case操作一个channel,要么读要么写
- select语句中除default外,各case的执行顺序是完全随机的
- select中如果没有default语句,则会阻塞等待任一case
- select语句中读操作要判断是否成功读取,关闭的channel也可以读取
6.实例
package main
import (
"fmt"
)
// write Data
func writeData(intChan chan int) {
for i := 0; i <= 50; i++ {
intChan <- i
fmt.Println("writeData 写入数据:", i)
}
close(intChan)
}
func readData(intChan chan int, exitChan chan bool) {
for {
v, ok := <-intChan
if !ok {
break
}
fmt.Println("readData 读取数据:", v)
}
exitChan <- true
close(exitChan)
}
func main() {
intChan := make(chan int, 50)
exitChan := make(chan bool, 1)
// 开启协程
go writeData(intChan)
go readData(intChan, exitChan)
for {
_, ok := <-exitChan
if !ok {
break
}
}
}
四、面向对象编程
1.继承
1.方法
Go中的方法式作用在指定的数据类型上的,因此自定义类型,都可以有方法
- 方法只能通过绑定的类型来调用,不能直接调用
- 为了提高效率,通常方法和结构体的指针类型绑定
1.定义
// 结构体添加方法
type A struct {
num int
}
func (a A) test {
fmt.Println(a.num) // 注意这里是值传递,如果想改变的话需要使用地址
}
type StringSet func(in int) bool
func (ff FilterFunc) Filte(in int) bool {
return ff(in)
}
2.方法的调用和传参机制
type Book struct {
pages int
}
func (b *Book) SetPages(pages int) {
b.pages = pages
}
func main() {
var book Book
// 调用隐式声明的函数
(*Book).SetPages(&book, 123)
book.SetPage(333)
}
3.方法与函数的区别
- 调用方式不同
- 函数的调用:函数名(实参列表)
- 方法的调用:变量.方法名(实参列表)
- 接收类型有差异
- 对于普通函数,接受类型为值类型时,不能传递指针类型,反之亦然
- 对于方法,接收值或者指针都可以