Golang的诞生及优势
Golang语言发布于2009年,是一门由Google支持的开源的静态编译型语言。作者是3位资深的语言专家:Robert Griesemer,Rob Pike,Ken Thompson。综合了各流行语言的优势,总体很接近C语言体系,主要优点如下:
- 简单易学
- 开发速度快
- 性能好
- 并发支持好
Docker、Kubernetes(K8s)、Etcd、Consul、Codis、Tidb等优秀的项目都是基于Golang开发的
容器:数组、切片、映射
数组(Array)
类型定义
数组的类型,取决于数组中的元素类型和数组的长度,其中数组的长度不可变;
[N]T
,N
表示数组的长度,T
表示数组中的元素类型,比如[10]int
var num1 [10]int
var num2 [5]int
var num3 [5]int32
reflect.Typeof(num1) == reflect.Typeof(num2)
数组变量声明及初始化
第二种方式只能在函数内使用
var num1 [3]int // num1 := [3]int{}
num2 := [3]int{1,2,3}
num3 := [...]int{1,2,3}
数组类型是值类型,不是引用类型, 数组类型是值类型,不是引用类型
func main() {
var data [3]int
var data2 [2]string
data3 := [...]string{"hello","world"}
fmt.Println(reflect.TypeOf(data) == reflect.TypeOf(data2)) // 1
fmt.Println(reflect.TypeOf(data2) == reflect.TypeOf(data3)) // 2
initNum(data)
fmt.Println(data) // 4 [0 0 0]
}
func initNum(num [3]int) {
for i := 0; i < len(num); i++ {
num[i] = i
}
fmt.Println(num) // 3 [0 1 2]
}
切片(Slice)
类型定义
[]T
,T
表示切片中的元素类型- 切片包含了对一段儿底层数组的动态引用,以及长度和容量
- 长度表示已经有几个元素、容量表示一共能放多少元素
type Slice struct {
Data unsafe.Pointer
Len int
Cap int
}
切片变量定义的一般方法
使用make函数,make的参数含义
var aa []int
aa = make([]int, 4) // 长度和容量都是4
bb := make([]int, 4, 6)
切片变量定义的其他方法
直接从已知数组或者切片中得到,注意越界问题
var num = [10]int{0,1,2,3,4,5,6,7,8,9}
slice1 := num[2:5:8] // [2,3,4], len=3, cap=6
slice2 := num[2:5] // 及其他写法 cap=8
slice3 := num[2:]
slice4 := num[:5] // [0 1 2 3 4]
slice5 := num[:]
注意避免两个slice相互影响,例如:
slice1 := []int{1,2,3,4,5,6} slice2 := slice1[1:4] slice3 := slice1[2:6] slice2[3] =6
更推荐的方法
func copySlice() {
slice1 := []int{1,2,3,4,5,6}
slice2 := make([]int, 3)
copy(slice2, slice1[1:4]) // copy(dst, src)
slice3 := make([]int, 4)
copy(slice3, slice1[2:6])
}
func some() {
slice1 := make([]int, 0, 2)
slice1 = append(slice1, 1,2,3)
}
常用函数append(slice, ...)
, len(slice)
, cap(slice)
, copy(dst, src)
切片的扩容
- 当切片长度等于容量时,再向切片中添加元素,会引发切片扩容
- 容量在1024以下,扩容每次乘2;否则,每次容量乘1.25
- 发生扩容时,Go就会开辟一块新的内存,把原来的值拷贝过来
下面的words
,len(words)
和cap(words)
是多少?
func main() {
words := make([]string, 1, 3)
words = append(words, "word1", "word2") // 3 3
words = append(words, "word3") // 4 6
}
看个例子
func main() {
var num = [10]int{0,1,2,3,4,5,6,7,8,9}
slice := num[2:5:8] // [2 3 4] 3 6
doubleNum(slice) // 传递的是指针
fmt.Println(slice) // 2 [4 6 8]
doubleNum2(slice)
fmt.Println(slice) // 4
}
func doubleNum(slice []int) {
for i := 0; i < len(slice); i++ { // 注意,这里不能用cap(slice)
slice[i] = slice[i] * 2
}
fmt.Println(slice) // 1 [4 6 8]
}
func doubleNum2(slice []int) {
for i, n := range slice {
n = n * 2 // 注意for-range是值复制,不会对slice产生实际影响
slice[i] = slice[i] * 2
}
fmt.Println(slice) // 3
}
映射
- 映射的定义:
map[K]T
- key、value结构
- delele,删除元素
func main() {
students := make(map[string]int)
fmt.Println(students, len(students))
students["Michael"] = 12
students["Tom"] = 10
students["Jack"] = 11
fmt.Println(students, len(students)) // map[Jack:11 Michael:12 Tom:10] 3
delete(students, "Jack")
fmt.Println(students, len(students)) // map[Michael:12 Tom:10] 2
for k, v := range students {
}
}
Golang没有内置集合(Set)类型,用Map
来实现Set
, 数组、切片、映射这几个容器都不是并发安全的
结构体与接口
结构体(Struct)
struct 用来自定义复杂数据结构,可以包含多个字段(属性),可以嵌套其他结构体,也可以定义方法.
结构体的定义
- 如何实现封装? 首字母大写或者小写
- 如何实现继承? 可以用结构体组合的方式
- 结构体的tag
type Gender int
const (
Female Gender = 0
Male Gender = 1
)
type Person struct {
Name string `json:"person_name"`
Age int32
Sex Gender
}
结构体的方法, 方法与函数的区别
func (p Person) ShowInfo() {
if p.Sex == Female {
fmt.Printf("Her name is %s and she is %d ages old", p.Name, p.Age)
}else {
fmt.Printf("His name is %s and he is %d ages old", p.Name, p.Age)
}
}
对结构体的初始化方法
- 直接初始化
- 使用
new
初始化person := new(Person)
,返回的是指针 new
与make
的区别
func main() {
person := Person{
Name : "Michael",
Age : 17,
Sex : Male,
}
person2 := &Person{
Name : "Michael",
Age : 17,
Sex : Male,
}
person3 := new(Person)
person3.Name = "Michael"
person3.Age = 17
person3.Sex = Male
person.Name = "Tom"
}
结构体方法的使用
func (p Person) ShowInfo() {
if p.Sex == Female {
fmt.Printf("Her name is %s and she is %d ages old", p.Name, p.Age)
}else {
fmt.Printf("His name is %s and he is %d ages old", p.Name, p.Age)
}
}
func (p *Person) SetAge(age int32) {
p.Age = age
}
func main() {
person := Person{
Name : "Michael",
Age : 17,
Sex : Male,
}
person.SetAge(18)
person.ShowInfo()
}
接口(Interface)
接口描述了某个类型有哪些方法。或者说一个接口类型,定义了一个方法集。通过接口可以轻松实现多态
type Animal interface {
Shout()
}
type Dog struct{}
type Cat struct{}
func (d Dog) Shout() {
fmt.Println("汪汪")
}
func (c Cat) Shout() {
fmt.Println("喵")
}
空接口可以被认为是很多其它语言中的any
类型, 空接口中没有任何方法,所以任何类型都实现了空接口, Println
为啥什么都能打印
func Println(a ...interface{}) (n int, err error)
类型断言
Plaintext
func main() {
// 编译器将把123的类型推断为内置类型int。
var x interface{} = 123
// 情形一:
n, ok := x.(int)
fmt.Println(n, ok) // 123 true
n = x.(int)
fmt.Println(n) // 123
// 情形二:
a, ok := x.(float64)
fmt.Println(a, ok) // 0 false
// 情形三:
a = x.(float64) // 将产生一个恐慌
}
协程与管道
协程(Goroutine)
Go不直接支持创建系统线程,协程是Go程序内部唯一的并发实现方式.
func main() {
fmt.Println("主协程开始")
go func() {
time.Sleep(1 * time.Second)
fmt.Println("打印日志")
time.Sleep(1 * time.Second)
}()
time.Sleep(3 * time.Second)
fmt.Println("主协程结束")
}
注意,主协程(main)结束后,此程序也就退出了,即使还有一些其它协程在运行.
协程的底层是如何实现的?
- 基于GMP模型
- G: goroutines 表示一个协程
- M: machine 表示一个线程
- P:Processor 管理器,通过队列管理协程
- 基于GMP模型,协程运行在线程上
- 一个协程中的信息:运行栈+寄存器数值(PC、BP、SP)
- 协程的切换,仅仅需要改变寄存器的数值,cpu便会从需要切换的协程指定位置继续运行
- 协程与线程比例关系N:M
协程:线程 | 含义 | 优点 | 缺点 |
---|---|---|---|
1:1 | 一个协程在一个线程运行 (其实就是传统的多线程) | 利用多核 | 上下文切换比较慢,为什么? |
N:1 | 多个协程在一个线程上运行 | 上下文切换较快 | 1、无法充分利用多核 2、饥饿,如果一个协程不结束,其余协程阻塞 |
N:M | 多个协程在多个线程上运行 | 充分利用多核,上下文切换快 |
协程的调度器的设计策略(减少开销兼顾公平):
- 复用线程(避免频繁的创建、销毁线程,而是对线程的复用)
- work stealing机制:当本线程无可运行的G时,尝试从其他线程绑定的P偷取G,而不是销毁线程。
- hand off机制:当本线程因为G进行系统调用阻塞时,线程释放绑定的P,把P转移给其他空闲的线程执行。
- 利用并行:GOMAXPROCS设置P的数量
- 抢占:限制协程执行时长,不会出现饿死现象
- 全局协程队列:链表实现
常用的同步控制机制:WaitGroup
开发过程中,经常遇到多task之间的同步问题。例如,多个子task并发完成一部分任务,主task等待他们最后结束。类似于Java的CountDownLatch
func main() {
var wg sync.WaitGroup ;
for i := 0; i < 3; i++ {
wg.Add(1)
go func(i int) {
t := rand.Intn(3)
time.Sleep(time.Duration(t) * time.Second)
fmt.Printf("The %d thing is Done.\n", i)
wg.Done()
}(i)
}
wg.Wait()
fmt.Println("Finished!")
}
管道(Channe)
并发模型CSP,全称Communicating Sequential Processes。它的核心观念是将两个并发执行的实体通过管道连接起来,所有的消息都通过管道传输。
管道(通道),也是一种Golang的数据同步技术。它可以被看作是在一个程序内部的一个先进先出(FIFO:first in first out)数据队列。
- 管道的操作有:读、写和关闭。
- 定义:
ch := make(chan string)
- 读:
a = <- ch
- 写:
ch <- "hello"
- 写一个已经关闭的channel会引发Panic。
无缓冲管道:长度为0的channel,为不带buffer的channel
ch := make(chan int, 10)
- 会发生额外的拷贝
- 写在读前 ch <- 1
- 缓冲区最大为65535
管道元素的传递,是复制,非缓冲区管道复制了1次,缓冲区管道复制了2次。
一个交替打印AB的例子:
import "fmt"
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
ch3 := make(chan string)
go printA(ch1, ch2)
go printB(ch1, ch2, ch3)
<-ch3
}
func printA(ch1, ch2 chan string) {
for i := 0; i < 100; i++ {
<-ch2
fmt.Println(i, "A")
ch1<- "print A"
}
}
func printB(ch1, ch2, ch3 chan string) {
ch2 <- "begin"
for i := 0; i < 100; i++ {
<-ch1
fmt.Println(i, "B")
if i != 99 {
ch2 <- "print B"
}else {
ch3 <- "end"
}
}
}
0 条评论