1、语法
(1)变量定义和初始化
var a int //标准,类型放后面:var 变量名 类型
a = 10
var a int = 10 //标准
var a = 10 //省略type
a,b := 10,"s" //短变量声明,最简化,【只能在函数内用,函数外不可以】。注意:直接a=10会报错undefined,:=左侧是新变量,=左侧是已声明变量
(2)bool类型
与C或Java不同,Go的整型和布尔型之间压根就没关系。
Go语言中不允许将整型强制转换为布尔型
布尔值并不会隐式转换为数字值 0 或 1,反之亦然
(3)整型
a := 3.1415e2 // 314.15,科学计数法表示
b := int64(a) // 强转语法
(4)常量
3.6.1. iota 常量生成器
3.6.2. 无类型常量
参考:
https://docs.hacknode.org/gopl-zh/ch3/ch3-06.html
(5)交换
a,b = b,ap
(6)defer
https://www.cnblogs.com/phpper/p/11984161.html
defer特性:
1. 关键字 defer 用于注册延迟调用。
2. 这些调用直到 return 前才被执。因此,可以用来做资源清理。
3. 多个defer语句,按[先进后出]的方式执行。
4. defer语句中的变量,在defer声明时就决定了。
例子:
//所有defer均在return前执行,defer之间的顺序按照栈先进后出
func Test2() {
defer f1(1)
var s, sep string
for i := 1; i < len(os.Args); i++ {
s += sep + os.Args[i]
sep = ","
fmt.Println(s)
}
defer f1(2)
fmt.Println(s, "2 3之间")
defer f1(3)
}
func f1(index int) {
fmt.Println("这里是defer的f1():", index)
}
运行命令:PS xx> go run main.go ddk dga sldkj
输出:
ddk
ddk,dga
ddk,dga,sldkj
ddk,dga,sldkj 2 3之间
这里是defer的f1(): 3
这里是defer的f1(): 2
这里是defer的f1(): 1
defer用途:
1. 关闭文件句柄
2. 锁资源释放
3. 数据库连接释放
潜在问题
https://www.jianshu.com/p/f897e69f5504
defer x.Close()
会忽略它的返回值,但在执行 x.Close()
时,我们并不能保证 x
一定能正常关闭,万一它返回错误应该怎么办?这种写法,会让程序有可能出现非常难以排查的错误。
(7)Go 语言 for 循环
Go 语言的 For 循环有 3 种形式,只有其中的一种使用分号。
和 C 语言的 for 一样:
包括for i 和 for range两种形式。
// for i
for init; condition; post { }
// for range
// for 循环的 range 格式可以对 slice、map、数组、字符串等进行迭代循环。格式如下:
for key, value := range oldMap {
newMap[key] = value
}
和 C 的 while 一样:
for condition { }
和 C 的 for( ; ; ) 一样:
for { }
- init: 一般为赋值表达式,给控制变量赋初值;
- condition: 关系表达式或逻辑表达式,循环控制条件;
- post: 一般为赋值表达式,给控制变量增量或减量。
2、复合数据类型
(1)数组
数组和Slice区别:指定长度为数组,不指定长度为切片。
定义数组:
var variable_name [SIZE] variable_type // 格式
var balance [10] float32
(2)Slice
定义切片,两种方式:
var identifier []type // 不指定长度。identifier := []int,会报错!
var slice1 []type = make([]type, len)
slice1 := make([]type, len) // make创建,指定长度
slice2 := make([]T, length, capacity) // 也可以指定容量capacity,可选。 默认初始为[0,0,...]。
// 补充:
capacity vs length:slice是个fat array,正是因为它有len和cap属性才能实现动态扩缩。
length:已使用的长度
capacity:容量
假如没有cap,只有len,怎么知道是否需要扩容呢?
但是一般都不怎么需要关注cap。
特殊场景关注cap:https://github.com/valyala/bytebufferpool
初始化:
s :=[] int {1,2,3}
s := arr[:] // 初始化切片 s,是数组 arr 的引用。
s := arr[startIndex:endIndex] // 将 arr 中从下标 startIndex 到 endIndex-1 下的元素创建为一个新的切片。
s := arr[startIndex:] // 默认 endIndex 时将表示一直到 arr 的最后一个元素。
s := arr[:endIndex] // 默认 startIndex 时将表示从 arr 的第一个元素开始。
初始化地址:
a := make([]int, 2)
println(a) // 实质就是&a[0],前面加个len
println(&a[0])
println(&a[1])
println(&a)
输出结果:
[2/2]0xc00003ff48
0xc00003ff48
0xc00003ff50
0xc00003ff60
(3)Map
-
map类型可以写为map[K]V
-
其中K对应的key必须是支持==比较运算符的数据类型,所以map可以通过测试key是否相等来判断是否已经存在
-
浮点数最好不要作为key:类型也是支持相等运算符比较的,但是将浮点数用做key类型则是一个坏的想法,正如第三章提到的,最坏的情况是可能出现的NaN和任何浮点数都不相等
-
创建map
// 1. 创建+赋值:内置的make函数 ages := make(map[string]int) ages["alice"] = 31 ages["charlie"] = 34 // 2. 创建+初始化: age2 := map[string]int{ "alice": 31, "charlie": 34, } //3. 创建空map age3 := map[string]int{}
-
访问map
// Map中的元素通过key对应的下标语法访问: ages["alice"] = 32 fmt.Println(ages["alice"]) // 使用内置的delete函数可以删除元素: delete(ages, "alice")
-
map作为参数
//map作为函数形参
func mapAsParam(params map[string]string, key string) string {
return params[key]
}
//map初始化并传参调用
func main(){
funcParams := map[string]string{"Name": "mapAsParam", "Type": "func", "Params": "{params,key}", "ParamsNum": "2", "ReturnTypes": "string", "DescribeFunction": "根据map的key返回value"}
key1 := "Name"
fmt.Println("当前对象是", mapAsParam(funcParams, key1), ",其诸多性质如下:")
for funcField := range funcParams {
fmt.Println(funcField, ":", funcParams[funcField])
}
}
(4)结构体
一个命名为S的结构体
类型将不能再包含S类型
的成员:因为一个聚合的值不能包含它自身。(该限制同样适用于数组)
但是S类型的结构体可以包含*S指针类型
的成员,这可以让我们创建递归的数据结构,比如链表和树结构等。
创建一个结构体变量
// 方法1。 &Point{1, 2}写法可以直接在表达式中使用,比如一个函数调用。
pp := &Point{1,2}
// 方法2
pp := new(Point)
*pp = Point{1,2}
结构体赋值,如果换行,最后要有",",如果不换行,最后不要有","
type a struct {
AA string
BB string
}
var S1 = a{
AA: "ddd",
BB: "sss", // 最后有逗号
}
var S2 = a{AA: "ddd", BB: "sss"}// 最后没有逗号
结构体作为参数
结构体可以作为函数的参数和返回值。
如果要在函数内部修改结构体成员的话,用指针传入是必须的;因为在Go语言中,所有的函数参数都是值拷贝传入的,函数参数将不再是函数调用时的原始变量。
Go语言类型别名 vs 类型定义
参考:
https://studygolang.com/articles/22667?fr=sidebar
http://www.weixueyuan.net/a/481.html
- 类型别名是 Go1.9 版本添加的新功能。之前都是类型定义。
- 类型定义会形成一种新的类型,编译后依然有该类型。
- 别名类型只会在代码中存在,编译后不会有别名类型。
type myInt int // 类型定义,a 的类型是 main.myInt,表示main 包下定义的myInt 类型。
type intAlias = int // 类型别名,b 的类型是 int 。intAlias 类型只会在代码中存在,编译完成时,不会有 intAlias 类型
(5)JSON
(6)文本和HTML模板
3、函数
func name(parameter-list) (result-list) {
body
}
(1)Go语言没有默认参数值
(2)没有函数体的函数声明,这表示该函数不是以Go实现的。这样的声明定义了函数标识符。
package math
func Sin(x float64) float //implemented in assembly language
(3)大写字母开头是公有,小写字母开头为私有。
(4)函数是一等公民(First-class value),即函数可以作为另一个函数的返回值或参数,还可以作为一个变量的值。函数可以嵌套定义【go语言仅支持匿名嵌套,不支持显式嵌套】,即在一个函数内部可以定义另一个函数。
4、方法
(1)接收器(receiver):相当于Java中的this或Python中的self,可以任意的选择接收器的名字。
(2)类的方法就是在函数名前加个该类的接收器:
type Point struct{ X, Y float64 }
// 全局函数
func Distance(p, q Point) float64 {
return math.Hypot(q.X-p.X, q.Y-p.Y)
}
// Point类型的方法Distance。p是接收器形参名,类Point是接收器类型。
func (p Point) Distance(q Point) float64 {
return math.Hypot(q.X-p.X, q.Y-p.Y)
}
(3)函数调用,会对每一个参数值进行拷贝,参数太大可以用指针避免这种默认拷贝。接收器也是参数。当接收器太大时,可以用其指针而不是对象来声明方法:
func (p *Point) ScaleBy(factor float64) {
p.X *= factor
p.Y *= factor
}
(4)在现实的程序里,一般会约定如果Point这个类有一个指针作为接收器的方法,那么所有Point的方法都必须有一个指针接收器,即使是那些并不需要这个指针接收器的函数。
一个方法用的是指针接收器,所有方法都要是指针接收器。
5、接口interface
data := map[string]interface{}{
"lang": "GO语言",
"tag": "<br>",
}
6、第八章 Goroutines和Channels
6.1引言
- Go语言中的并发程序可以用两种手段来实现,分别是第八章和第九章。
- 本章讲解goroutine和channel,其支持“顺序通信进程”(communicating sequential processes)或被简称为CSP。
- CSP是一种现代的并发编程模型,在这种编程模型中值会在不同的运行实例(goroutine)中传递,尽管大多数情况下仍然是被限制在单一实例中。
- 第9章覆盖更为传统的并发模型:多线程共享内存,如果你在其它的主流语言中写过并发程序的话可能会更熟悉一些。
6.2
6.3
6.4Channels
创建channel
ch := make(chan int) // make函数,我们可以创建一个channel
操作channel
发送和接收两个操作都使用<-
运算符
ch <- x // 把x发送到channel中
x = <- ch // x接收channel中的值
<- ch // 接收channel值,匿名,eg, fmt.Println(<- ch)
channel死锁(deadlock)的情况
(1)声明channel时,没有说明具体的大小,会导致在存储数据时死锁
//make(chan type ,size)//第二个参数为存储的大小
//正确情况:指定尺寸为1,同一个线程就可以读写同一个channel,不会死锁。
func main() {
ch := make(chan int, 1)
ch <- 1
fmt.Println(<-ch)
fmt.Println("lalalala")
time.Sleep(3 * time.Second)
}
// 运行结果:
1
lalalala
下面是不指定size时,几种死锁的情况,并且采用多线程解决:
func main() {
//情况1:不用多线程,报错
ch := make(chan int) // 没有指定size,导致死锁。
//ch <- 10 //直接报错,因为只有一个主线程,ch存入一个元素后就阻塞了主线程。
}
//运行结果:
fatal error: all goroutines are asleep - deadlock!
func main() {
//情况2:1个多线程写入
ch2 := make(chan int) //1 没有指定size,导致死锁。
go func() { //2 4
fmt.Println("开启协程1") //5
ch2 <- 10 //7 6 写入channel死锁,协程1被阻塞。
fmt.Println("协程1复活") //8 从这行往后都无法输出了
fmt.Println("协程1从ch2读:", <-ch2) //9 10 8
}() //13 12
//虽然上面的协程已经死锁,但是主线程没有死锁,所以仍然执行,并且继续输出。
fmt.Println("主线程不受影响") //3
fmt.Println("主线程从ch2读:", <-ch2) //6 7 主线程可以直接读channel
ch2 <- 5 //10 9 7 主线程写入channel,主线程被阻塞
fmt.Println("主线程最后一行") //11 9
time.Sleep(3 * time.Second) //12 13
} //14
//运行结果:
开启协程1
主线程不受影响
主线程从ch2读: 10
协程1复活
主线程最后一行
协程1从ch2读: 5
或者:
主线程不受影响
开启协程1
协程1复活
主线程从ch2读: 10
主线程最后一行
协程1从ch2读: 5
func main() {
//情况3:1个线程写入,1个线程读出。这样即使写线程阻塞,读线程仍不受影响,一旦channel被读出了,写入线程就可以继续写入了!
ch := make(chan int) // 没有指定size,导致死锁。
time.Sleep(3 * time.Second)
(2)channel存储满后,如果再进行存储,会导致线程锁住,只能等到channel将数据取出之后,才能进行正常存储
a:=make(chan int ,1)
a<-1
a<-2
<-a
1
2
3
4
此时,如果没有<-a,会导致通道的阻塞,只能等到<-a才能再存储
3:channel中没有值时,进行读取数据,不能正常读取,造成通道的阻塞
<-a
如果不赋值,直接从channel中读取数据,会造成通道的阻塞
原文链接:https://blog.csdn.net/Xiang_lhh/article/details/108779630
关闭channel
(1)关闭后,对基于该channel的任何发送操作都将导致panic异常。
(2)对一个已经被close过的channel进行接收操作依然可以接受到之前已经成功发送的数据;如果channel中已经没有数据的话将产生一个零值的数据。
close(ch) // 使用内置的close函数关闭channel
打开channel
参考:https://www.jianshu.com/p/bc7aa77a609c
go语言原生语法并没有提供方法打开channel,只能自己写方法,利用指针再次打开。
channel的结构体在chan.go
中:
type hchan struct {
qcount uint // total data in the queue
dataqsiz uint // size of the circular queue
buf unsafe.Pointer // points to an array of dataqsiz elements
elemsize uint16
closed uint32
//... 以下字段没有用上,先省略
}
Channel是否关闭取决于hchan.closed
,0是打开,1是关闭。
方法:让指针指向hchan.closed
直接修改它的值。
代码实现【不理解!!!】
//go:linkname lock runtime.lock
func lock(l *mutex)
//go:linkname unlock runtime.unlock
func unlock(l *mutex)
func open(c interface{}) error {
v := reflect.ValueOf(c)
if v.Type().Kind() != reflect.Chan {
return errors.New("type must be channel")
}
i := (*[2]uintptr)(unsafe.Pointer(&c)) //定位c所在数据空间,这里的c是个指针所以要进行一步取值
var closedOffset, lockOffset uintptr = 28, 88
closed := (*uint32)(unsafe.Pointer(i[1] + closedOffset)) //指向closed的地址
if *closed == 1 {
lockPtr := (*mutex)(unsafe.Pointer(i[1] + lockOffset)) //指向lock地址
lock(lockPtr) //上锁
if *closed == 1 {
*closed = 0 //直接修改值
}
unlock(lockPtr) //解锁
}
return nil
}
7、结构
(1)package包声明
package main //执行程序必须叫main,下面必须有main函数,作为入口
package test // 其他的包,不一定要与目录同名,但最好同名
(2)import引入包
// 方式1
import "fmt"
import "demo1/test" // 项目名/包名
// 方式2
import (
"fmt"
"demo1/test" // 项目名/包名
)
(3)函数
(4)变量
(5)语句 & 表达式
(6)注释
单行注释://
多行注释:/**/
9、大小写命名规范和访问权限
(1)首字母大写是公有的,首字母小写是私有的,因此,变量或函数只需要首字母大写即可跨包
访问
(2)golang的命名需要使用驼峰命名法,且不能出现下划线
(3)结构体中属性名最好大写:如果属性名小写则在数据解析(如json解析,或将结构体作为请求或访问参数)时无法解析
包名:小写单词,不要使用下划线或者混合大小写。
保持package的名字和目录保持一致,尽量采取有意义的包名,简短,有意义,尽量和标准库不要冲突。
文件名:小写单词,使用下划线分隔各个单词。
结构体/接口/变量/函数名:采用驼峰命名法,不可用下划线。首字母大写包外可访问,小写包内可访问。
变量名,但遇到特有名词时,需要遵循以下规则:
如果变量为私有,且特有名词为首个单词,则使用小写,如 appService
若变量类型为 bool 类型,则名称应以 Has, Is, Can 或 Allow 开头
常量名:全部大写字母组成,并使用下划线分词
参考:
https://www.cnblogs.com/rickiyang/p/11074174.html
10、运行
(1)直接运行
go run hello.go
(2)编译运行
go build hello.go
hello
作者:西伯尔
出处:http://www.cnblogs.com/sybil-hxl/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。