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

Slice传参

(3)Map

  1. map类型可以写为map[K]V

  2. 其中K对应的key必须是支持==比较运算符的数据类型,所以map可以通过测试key是否相等来判断是否已经存在

  3. 浮点数最好不要作为key:类型也是支持相等运算符比较的,但是将浮点数用做key类型则是一个坏的想法,正如第三章提到的,最坏的情况是可能出现的NaN和任何浮点数都不相等

  4. 创建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{}
    
  5. 访问map

    // Map中的元素通过key对应的下标语法访问:
    ages["alice"] = 32
    fmt.Println(ages["alice"]) 
    // 使用内置的delete函数可以删除元素:
    delete(ages, "alice")
    
  6. 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

  1. 类型别名是 Go1.9 版本添加的新功能。之前都是类型定义。
  2. 类型定义会形成一种新的类型,编译后依然有该类型。
  3. 别名类型只会在代码中存在,编译后不会有别名类型。
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引言

  1. Go语言中的并发程序可以用两种手段来实现,分别是第八章和第九章。
  2. 本章讲解goroutine和channel,其支持“顺序通信进程”(communicating sequential processes)或被简称为CSP。
  3. CSP是一种现代的并发编程模型,在这种编程模型中值会在不同的运行实例(goroutine)中传递,尽管大多数情况下仍然是被限制在单一实例中
  4. 第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

posted on 2021-12-21 15:36  西伯尔  阅读(384)  评论(0编辑  收藏  举报