这个学期选了Go语言与分布式开发课程,这篇博客是准备期末考试时所总结的笔记,内容比较基础。参考了github上的the-way-to-go。
声明了一定要使用,否则会报错
var name dataType
:立即分配内存空间,初始化为零值
var name = value
可在函数内部用如下形式初始化并赋值name := value
var a int
var b = 10
c := "yy"
fmt.Println(a)//0
fmt.Println(b)//10
fmt.Println(c)//yy
const name = value
const (
name1 = iota
name2
name3
)
fmt.Println(name1)//0
fmt.Println(name2)//1
fmt.Println(name3)//2
if条件语句中,条件外没有()
if 条件{
statement
}
go语言中不支持三元条件语句
if可带一个初始化子句,用;
和条件子句分隔开
if i:=5;i<=10{//使用【分号】将条件子句分开
statement
}
switch语句可以带初始化子句,用;
分隔开
switch score:=78;math.floor(score/10){//使用【分号】将条件子句分开
case 10:statement
case 9:statement
case 8:statement
case 7,6:statement//case后可以有多个值
default:statement
}
break可以省略
for后没有()
for i:=1;i<=10;i++{
statement
}
初始子句、判断子句、操作子句任意一个被省略了,;
都不能被省略
当只有判断子句时,可以不使用分号
三个子句都省略时,表示这时一个无限循环
goto end// 【跳至】end,执行end之后的语句a
end:
...a
end:
...a
break end//【跳出】end语句,执行之后的语句b
...b
int
、uint
分别对应有符号和无符号floa
t组成,complex64
:两个float32
complex(float,float)
real()
:获取实部image()
:获取虚部byte
:代表ASCIIrune
:代表Unicodechar
!字符串String
中:一个ASCII占一个字节、一个中文占三个字节
var name [length]dataType
:全部初始化为零值
name := [length]dataType{value1...}
:有初始化值的数组
name := [...]dataType{value1...}
:根据值的数量来确定数组的长度
name := [length]dataType{index:value...}
:将相应位置的数组初始化为相应的值,其余初始化为零值
数组的复制,以及函数传参的调用都是值拷贝
遍历
for i,v := range arr{// i是数组中的下标序号,v是数组相应序号的值
statement//只使用i或v中的一个会出现编译错误
}
for _,v := range arr{
statement//可以只使用v
}
切片在声明时,不能给定底层数组的长度,否则会变成数组声明。
//根据指定的数组创建切片
a := [5]int{1,2,3,4,5}// 只创建数组
b := [...]int{1,2,3,4,5}// 只创建数组
silce := a[1:2] //创建切片
var silce2 := a[2,3] //创建切片
//同时创建数组和切片
silce := []int{1,2,3,4,5}
声明语法var name []dataType
在声明之后创建切片变量,但底层数组指针是nil
,所以切片也是nil
也可使用make函数来声明和创建切片
i:=make([]int,5,5)//[0,0,0,0,0] i := make([]type, len, cap)
new函数用于普通类型数据的内存分配
make函数用于slice、map、channel类型数据的内存分配
len()
返回slice的长度;cap()
返回slice的底层数组的容量
多个切片可以共享同一个底层数组
使用函数append
可以扩展slice,同时也会直接覆盖掉底层数组上相对应的数据;使用函数append
可以合并两个slice
slice = append(slice, 100)//扩展slice,在slice尾部添加一个100,同时覆盖原数组相应位置的值
newSlice = append(slice1,slice2...)//合并两个slice
切片的函数传参是引用传递
多维切片:pls := [][]int{{1,2,3},{4,5},{6},}
每行的元素的个数可以不同
map是引用类型,不支持==
操作(除了和nil)
map中的键只支持值类型(支持==
操作)
map中的值不做限制,但是要求所有的值的类型都是同一种
go中的map的底层是哈希数组链表
var name map[keyType]valueType
:只声明不初始化,不能添加元素,并且此时name==nil
var m map[string]int //只声明
fmt.Println(m == nil) //true
name2 := name[keyType]valueType
:用字面量或make函数进行初始化后可以添加元素,name2[key]=value
m := map[string]int{}//字面量
fmt.Println(m == nil)//false
m["yy"] = 22//添加元素
fmt.Println(m["yy"])//22
m := make(map[string]int)//make函数
fmt.Println(m == nil)//false
m["yy"] = 22//添加元素
fmt.Println(m["yy"])//22
也可使用make函数来初始化name := make(map[keyType]valueType)
也可以在初始化的同时进行赋值:
m := map[String]int{//这时候一定要使用字面量
"1":1,
"2":2,
}
映射元素的查找:value,exist = m["1"]
,如果对应的key值存在,那么exist
为true,并且在value
中返回相对应的值;否则exist
为false
m := map[string]int{
"yy": 22,
}
if v, exist := m["yy"]; exist {//exist==true
fmt.Println(v)//22
}
if v, exist := m["ljl"]; exist {//exist==fasle
fmt.Println(v)
}
可以使用range遍历map,但是不保存顺序
for key,value := range m{
statement
}
删除键值对
delete(m,"1")//将m中的键“1”对应的键值对删除;如果对应的键不存在,那么什么都不发生
map的键的值也可以是map,即映射的映射
m := map[String]map[String]int{
"1":{
"2":2,
"3":3,
},
}
// m["1"]["2"]=2
func funcName(paramList)(returnList){// 返回值列表可以省略
codind...
}
func add(a,b int) int {// a和b都是int,多个相邻相同类型参数可以使用简写
return a+b
}
//返回值可以有变量名
//go不支持函数的重载
函数可以传递可变参数:形参的数目可变。
格式:在需要声明可变参数函数时, 只能在参数列表的最后一个参数类型之前加上省略符号“…”,这表示:该函数会接收任意数量的该类型参数。
func getSum(vals...int) int {//在参数列表的最后一个参数类型之前加上省略符号“...”
sum := 0
for _,v:= range vals{//不定参数的形参在函数内是【切片】
sum += v
}
return sum
}
如果原始参数已经是切片类型, 则需要在最后一个参数后加上省略符。 下面的两段代码作用相同:
int result1 = getSum(1,2,3,4)
int[] values = []int{1,2,3,4}
int result2 = getSum(values...)//原始参数已经是切片,需要在需要给切片添加省略号
函数类型又称函数签名。
显示函数类型:fmt.Printf("%T\n",funcname)
fmt.Printf("%T\n", getSum)//func(int, ...int) int
函数类型包括形参列表和返回值列表
函数类型的零值是nil
标准定义的函数名为常量,不可修改指向
函数是第一公民,函数变量可赋值、传参等
匿名函数相当于函数字面量,可以使用函数变量的地方就可以使用匿名函数
//匿名函数直接调用
func(a,b int)int{
return a-b
}(5,4)
//匿名函数赋值给函数变量
var sum = func(a,b int)int{
return a+b
}
sum(5,4)
//函数作为返回值
func getFun(op string) func(a,b int)int{
return func(a,b int)int{
return a+b
}
}
关键字defer
允许我们推迟到函数返回之前(或任意位置执行 return
语句之后)一刻才执行某个语句或函数
defer实参:在注册defer函数 时,会把当时的实参值传递给形参,后续实参的变化不影响函数结果,知道函数返回时(或者执行return)才进行执行
a := 5
defer fmt.Println(a)
a = 10
fmt.Println(a)
return
/*
10
5
*/
使用多个defer时,这些defer 调用以先进后出(FILO)顺序在函数返回前被执行
name := "Naveen"
fmt.Printf("Original String: %s\n", string(name)) //Original String: Naveen
fmt.Printf("Reversed String: ")
for _, v := range []rune(name) {
defer fmt.Printf("%c", v)//Reversed String: neevaN
}
type Employee struct{
firstName,lastName string//没有逗号
age,salary int
}
//匿名类型结构(直接创建结构变量):用var代替type
var Employee struct{
firstName,lastName string
age,salary int
}
//结构体中的字段除了名字和类型外,还可以有一个可选的标签(tag)用于描述结构体字段的信息
type Employee struct{
firstName,lastName string "姓名"
age,salary int
}
利用字段名初始化,不用按照顺序,没有被初始化的值默认为零值
e := Employee{
firstName: "y",//使用逗号
age: 22,
lastName: "y",
}
利用字面量进行初始化,需要按照顺序并且需要全部设置
e := Employee{"y","y",22,0}
可以使用new,返回指向该结构体变量的指针,初始值全部为零值
s := new(student)//s是指向student的指针
s.name = "yy"
s.age = 22
fmt.Println(*s)
e := Employee{"y","y",22,0}
fmt.Println(e.age)
e := &Employee{"y","y",22,0}
fmt.Println((*e).age) //不支持->
结构体字段也可以省略字段名,字段名默认为对应数据类型名称(数据类型不能重复)
type Student struct{
int
string
}
s := Student{22,"yy"}
fmt.Println(s.int)
type Address struct {
city, state string
}
type Person struct {
name string
age int
address Address//嵌套结构体
}
func main() {
var p Person
p.name = "Naveen"
p.age = 50
p.address = Address{
city: "Chicago",
state: "Illinois",
}
fmt.Println("Name:", p.name)
fmt.Println("Age:", p.age)
fmt.Println("City:", p.address.city)//调用嵌套结构体
fmt.Println("State:", p.address.state)
}
//使用匿名子结构字段,在没有重名的情况下,使得子结构体中的字段可以像父结构体中的字段一样被访问
type Address struct {
city, state string
}
type Person struct {
name string
age int
Address//匿名子结构体
}
func main() {
var p Person
p.name = "Naveen"
p.age = 50
p.address = Address{
city: "Chicago",
state: "Illinois",
}
fmt.Println("Name:", p.name)
fmt.Println("Age:", p.age)
fmt.Println("City:", p.city)//使得子结构体中的字段可以像父结构体中的字段一样被访问
fmt.Println("State:", p.state)
}
方法是对具体类型行为的封装,本质上是绑定到该类型的函数
【方法】的格式是:func (t type) funcName(paramList)(returnList){coding...}
【函数】的格式是:func funcName(paramList)((returnList){coding...}
例子:
type Employee struct {
name,currenc string
salary int
}
//定义方法
func (e Employee) displaySalary() {
fmt.Printf("Salary of %s is %s%d", e.name, e.currency, e.salary)
}
//如果是函数则是func displaySalary(e Employee){}
func main() {
emp1 := Employee{
name: "Sam Adolf",
salary: 5000,
currency: "$",
}
emp1.displaySalary()//Salary of Sam Adolf is $5000
}
//方法可以使用函数进行代替,但是使用方法可以解决函数不能重载的问题
func displaySalary(e Employee) {
fmt.Printf("Salary of %s is %s%d", e.name, e.currency, e.salary)
}
func main() {
emp1 := Employee{
name: "Sam Adolf",
salary: 5000,
currency: "$",
}
displaySalary(emp1)//Salary of Sam Adolf is $5000
}
注意事项:
方法接收者的本质是形参,需要在内存中复制一份对象,所以方法修改对象属性将不能成功
方法提升:匿名子结构的方法可以像父结构方法一样被使用
package main
import (
"fmt"
)
type add struct {
country string
city string
}
type student struct {
name string
age int
address add
}
func (a add) shuchu() {
// fmt.Printf(s.name + " " + s.age + " " + s.address.country + " " + s.address.city)
fmt.Printf("%s %s", a.country, a.city)
}
func main() {
var s student
s.name = "yy"
s.age = 22
s.address = add{
country: "china",
city: "ningbo",
}
s.address.shuchu()
}
方法并非结构体专有,所有自定义类型都可以定义方法
type interfaceName interface{
methodName(paramList)(returnList)
otherMethod
}
匿名接口的命名格式:
interface{
methodName(paramList)(returnList)
otherMethod
}
Go中的接口是可以有值的:var ai interfaceName
,本质是一个指针,初始值是nil。实现接口interfaceName
的变量可以赋值给ai
。
使用接口的例子
package main
import "fmt"
type Shaper interface {//定义一个接口Shaper,接口内有一个方法Area()
Area() float32
}
type Square struct {//定义一个结构体Square
side float32
}
func (sq *Square) Area() float32 {//结构体的方法,定义了方法Area(),实现了接口Shaper
return sq.side * sq.side
}
func main() {
sq1 := new(Square)//使用new返回一个指向Square的指针
sq1.side = 5
var areaIntf Shaper//定义一个接口变量
areaIntf = sq1//将实现了Shaper接口的结构体变量sq1复制给接口变量areaIntf
fmt.Printf("The square has area: %f\n", areaIntf.Area())//25
}
一个接口可以嵌套另一个或者多个接口
type ReadWrite interface {//ReadWrite接口
Read(b Buffer) bool
Write(b Buffer) bool
}
type File interface {//File接口中嵌套了ReadWrite接口
ReadWrite
Close()
}
v,ok := interfaceVar.(typeName)
/*
判断某个接口变量,是否为某个类型,interfaceVar必须是接口变量
是,则返回true和值
否,则返回false和零值
*/
接口类型断言的例子
package main
import (
"fmt"
"math"
)
type Square struct {//结构体Square
side float32
}
type Shaper interface {//接口Shaper
Area() float32
}
func main() {
var areaIntf Shaper//声明一个接口变量
sq1 := new(Square)//一个结构体变量
sq1.side = 5
areaIntf = sq1//初始化接口变量
//判断结构变量areaIntf是否为*Square
if t, ok := areaIntf.(*Square); ok {
fmt.Printf("The type of areaIntf is: %T\n", t)
}
}
func (sq *Square) Area() float32 {//结构体变量Square实现了接口Shaper
return sq.side * sq.side
}
func findType(i interface{}) {//空接口,所有类型都实现了空接口
switch t := i.(type) {//i是一个接口变量,注意:.(type)只能用于switch表达式i是一个接口变量
case string:
fmt.Printf("string and value is %s\n", i.(string))
case int:
fmt.Printf("int and value is %d\n", t)//用t来代替i.(int),通过t还可以调用接口中的方法
default:
fmt.Printf("Unknown type\n")
}
}
findType("Naveen")
findType(77)
findType(89.98)
//string and value is Naveen
// int and value is 77
// Unknown type
一个变量最基本的两个属性就是:类型和值。
反射可以在运行时检查变量的类型和值,是元编程的一种形式,在没有源代码时帮助调试程序。
反射包“reflect”通过空接口获取变量的类型和值
func TypeOf(i interface{}) Type
func ValueOf(i interface{}) Value
/*
reflect.TypeOf(x):返回变量x的类型
reflect.ValueOf(x):返回变量x的值
*/
reflect.Type
和 reflect.Value
都有许多方法用于检查和操作它们:Type
和Value
都有 Kind
方法返回一个常量来表示类型:Uint
、Float64
、Slice
等等。
package main
import (
"fmt"
"reflect"
)
func main() {
var x float64 = 3.4
fmt.Println("type:", reflect.TypeOf(x))//type: float64
v := reflect.ValueOf(x)
fmt.Println("value:", v)//value: 3.4
fmt.Println("type:", v.Type())//type: float64
fmt.Println("kind:", v.Kind())//kind: float64
}
package main
import (
"fmt"
"reflect"
)
type TagType struct {
field1 bool "1"
field2 string "2"
}
func main() {
tt := TagType{true, "Barak Obama"}
for i := 0; i < 2; i++ {
refTag(tt, i)
}
}
func refTag(tt TagType, ix int) {
ttType := reflect.TypeOf(tt)//获取tt的类型
ixField := ttType.Field(ix)//结构体变量可以通过Field来索引结构体的各个字段
fmt.Printf("%v\n", ixField.Tag)//输出各个字段的Tag属性
}
//1
//2
func add(a, b int) int {
return a + b
}
// 将 函数 包装为反射值对象
funcValue := reflect.ValueOf(add)
// 生成函数参数, 传入两个整型值
paramList := []reflect.Value{reflect.ValueOf(10), reflect.ValueOf(20)}
// 反射调用函数
retList := funcValue.Call(paramList)
// 获取第一个返回值, 取整数值
fmt.Println(retList[0].Int())//30
GO 语言里没有异常机制,只有错误处理,错误通过函数的多返回值来处理
GO 语言的错误主要有:
GO错误处理方式:
type error interface {
Error() string
}
可能出错的函数的最后一个返回值为错误类型
检查该返回值是否为nil:是,则没有出错;否,则出错。
f, err := os.Open("/test.txt")//os.Open可能出错,最后一个返回值为错误类型
if err != nil {//检查err是否为nil,不是nil则出错
fmt.Println(err)
return
}
fmt.Println(f.Name(), "opened successfully")//是nil,则没有出错
错误至少包含文本说明信息
部分错误通过结构体更加详尽地说明错误
type PathError struct {//定义一个结构体
Op string
Path string
Err error
}
//结构体实现了Error()接口
func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() }
例子:
f, err := os.Open("/test.txt")
if perr, ok := err.(*os.PathError); ok {
fmt.Println("File at path", perr.Path, "failed to open")
return
}
fmt.Println(f.Name(), "opened successfully")
有些错误则通过方法可以提供更多的信息
addr, err := net.LookupHost("www.abc.com")
if err, ok := err.(*net.DNSError); ok {
if err.Timeout() {//调用方法
fmt.Println("operation timed out")
} else if err.Temporary() {//调用方法
fmt.Println("temporary error")
} else {
fmt.Println("generic error: ", err)
}
return
}
fmt.Println(addr)
// 正常处理代码
err := firstCheckError()
if err != nil {
fmt.Println(err)
exitProcess()
return
}
err = secondCheckError()
if err != nil {
fmt.Println(err)
exitProcess()
return
}
// 使用goto集中处理错误
err := firstCheckError()
if err != nil {
goto onExit
}
err = secondCheckError()
if err != nil {
goto onExit
}
onExit:
fmt.Println(err)
exitProcess()
当遇到不可恢复的错误状态,导致程序不能继续执行,引发panic。该错误提供RuntimeError()
方法用于区别普通错误
引发panic的两种情况:
主动调用panic
函数,会产生一个运行时错误。panic
函数接收一个任意类型的参数,通常是字符串,在程序死亡时被打印出来。
package main
import "fmt"
func main() {
fmt.Println("Starting the program")
panic("A severe error occurred: stopping the program!")
fmt.Println("Ending the program")
}
程序运行时出现未处理错误自动触发,比如当发生像数组下标越界或类型断言失败等运行时错误时,Go 运行时会自动触发panic
不应通过调用panic
函数来报告普通的错误,而应该只把它作为报告致命错误的一种方式
在多层嵌套的函数调用中调用panic
,可以马上中止当前函数的执行,所有的 defer
语句都会保证执行,并把控制权交还给接收到 panic
的函数调用者。这样向上冒泡直到最顶层,并执行(每层的)defer
,在栈顶处程序崩溃,并在命令行中用传给panic
的值报告错误情况。
panic
一旦被引发就会导致程序崩溃,但无法保证程序不会发生任何运行时错误。
recover
专用于“拦截”运行时panic,让进入恐慌的程序恢复过来并重新获得流程控制权。
recover
可以阻止panic继续向上传递
为确保捕获panic
,recover
必须在defer函数中执行
func protect(g func()) {
defer func() {
log.Println("done")
// 即使出现panic,printF也会继续执行
if err := recover(); err != nil {
log.Printf("run time panic: %v", err)
}
}()
log.Println("start")
g() // possible runtime-error
}
panic
或由运行时抛出panic
recove
捕获panic
通道是一种特殊的类型,同时只能有一个 goroutine 访问通道进行发送和获取数据;
通道是一个队列,遵循先入先出(FIFO)的规则;
通道默认是阻塞的,使goroutine有效通信;
通道是引用类型,需要使用chan关键字和内置函数make 进行创建
通道写入和读取使用 <-
运算符
通道<-变量
变量<-通道
缓冲通道
通道包括无缓冲通道和有缓冲通道:
无缓冲通道:只能存储一条消息: make(chan datatype)
有缓冲通道 make(chan datatype,capacity)
,可以根据capacity
参数存储多条消息,按FIFO的原则进行读取
func receiver(c chan string) {
for msg := range c {
fmt.Println(msg)
}
}
func main() {
messages := make(chan string, 2)//创建一个有缓冲通道messages,容量=2
messages <- "hello"
messages <- "world"
go receiver(messages)
time.Sleep(1e9)
}//hello world
获取缓冲通道的状态:len()
:获取通道当前缓存数;cap()
:获取通道缓存容量
ch := make(chan string, 3)
ch <- "1"
ch <- "2"
fmt.Println(cap(ch)) // 3
fmt.Println(len(ch)) // 2
fmt.Println("read value: ", <-ch) //1
fmt.Println(cap(ch)) // 3
fmt.Println(len(ch)) // 1
缓冲和阻塞
无缓冲通道,写入等待读取,读取等待写入,在双方准备好之前是阻塞的
有缓冲通道,在通道已满时:写入会等待;通道已空时候:读取会等待
死锁
通道无数据或未写入时,未启动写入协程 就进行读取
通道已满时,未启动读取协程 就继续写入
单向通道
通道默认为双向的,单向通道只能用于发送或接收数据
所谓单向通道只是对通道作为函数参数的一种使用限制。通常先创建双向通道,在函数形参中利用<-
运算符修饰通道,使之变为只读或只写通道
func pump(ch chan<- int) //只写
func pull(ch <-chan int) //只读
关闭通道
关闭通道使用内置函数close()
,实际上是关闭写入,即发送者告诉接收者不会再有数据发往通道
接收者能够在通道接收数据的同时,获取通道是否已关闭的参数
func producer(chnl chan int) {
for i := 0; i < 10; i++ {
chnl <- i
}
close(chnl)
}
func main() {
ch := make(chan int)//建立无缓冲通道
go producer(ch)
for {
v, ok := <-ch//接收数据的同时,获取通道是否已经关闭的参数
if ok == false {
break
}
fmt.Println("Received ", v, ok)
}
}
for range
语句能自动判断通道是否已关闭
func producer(chnl chan int) {
for i := 0; i < 10; i++ {
chnl <- i
}
close(chnl)
}
func main() {
ch := make(chan int)
go producer(ch)
for v := range ch {
fmt.Println("Received ",v)
}
}
一次性定时器:定时器只计时一次,结束便停止
周期性定时器:定时器周期性进行计时,除非主动停止,否则将永久运行
GO使用包来组织源代码和代码编译,实现代码复用
任何源代码必须属于某个包,同时源码文件的第一行有效代码必须是package pacakgeName
语句,声明自己所在的包。
包名为 main
是应用程序的入口包,编译不包含 main
包的源码文件时不会得到可执行文件
一个文件夹下的所有源码文件只能属于同一个包,同样属于同一个包的源码文件不能放在多个文件夹下
GOROOT:GO语言环境在计算机的安装位置,包含GO标准库的源代码
GOPATH GO语言工作目录,可以有多个。GOPATH指定的工作目录通常包括3个子目录:
GOPATH太不方便,在项目目录下用go.mod 文件来记录依赖包具体版本,方便依赖包、源代码和版本控制的管理。go.mod文件的内容:
包的导入:
使用 import "包的路径"
导入使用的包
包的路径使用/ 进行分隔:GOROOT/src/或GOPATH/src/或 go.mod代表的目录后面包的存放路径,比如"fmt"
位于GOROOT/src/fmt 目录,"crypto/sha256"
位于GOROOT/src/crypto/sha256 目录
单行导入
import "crypto/sha256"
import "fmt"
import "math/big"
多行导入
import (//没有逗号
"crypto/sha256"
"fmt"
"math/big"
)
包的引用格式:
标准引用格式
import "fmt"
fmt.Printf("Hello world!")
自定义别名引用格式
import F "fmt"
F.Printf("Hello world!")
省略引用格式
import ."fmt"
Printf("Hello world!")
匿名引用格式
引用包,在代码中却没有进行使用,编译器会报错。所以,如果只是希望执行包初始化的init
函数,而不使用包内部的数据时,可以使用匿名引用格式
import _ "fmt"
包内标识符的导出