go mod init namecost
go mod tidy
go run main.go
函数外的每个语句都必须以关键字开始(var、const、func等)
:=不能使用在函数外。
_多用于占位,表示忽略值。
const (
n1 = 100
n2
n3
)
const (
n1 = iota //0
n2 = 100 //100
n3 = iota //2
n4 //3
)
const n5 = iota //0
const (
a, b = iota + 1, iota + 2 //1,2
c, d //2,3
e, f //3,4
)
iota是go语言的常量计数器,只能在常量的表达式中使用。 iota在const关键字出现时将被重置为0。const中每新增一行常量声明将使iota计数一次(iota可理解为const语句块中的行索引)。 使用iota能简化定义,在定义枚举时很有用。
const (
_ = iota
KB = 1 << (10 * iota)
MB = 1 << (10 * iota)
GB = 1 << (10 * iota)
TB = 1 << (10 * iota)
PB = 1 << (10 * iota)
)
定义数量级 (这里的<<表示左移操作,1<<10表示将1的二进制表示向左移10位,也就是由1变成了10000000000,也就是十进制的1024。同理2<<2表示将2的二进制表示向左移2位,也就是由10变成了1000,也就是十进制的8。)
go 语言有没有线程安全的数据类型
说的挺全了:锁、chan 、sync.Map, 还是不要并发好点,简单。
每个变量在程序中都有一定的作用范围,称之为作用域。如果一个变量在函数体外声明,则被认为是全局变量,可以在整个包甚至外部包(变量名以大写字母开头)使用,不管你声明在哪个源文件里或在哪个源文件里调用该变量。在函数体内声明的变量称之为局部变量,它们的作用域只在函数体内,函数的参数和返回值变量也是局部变量
““““““““““`
需要注意的是,字典初始化之后才能进行赋值操作,如果仅仅是声明,此时 testMap 的值为 nil,在 nil 上进行操作编译期间会报 panic(运行时恐慌),导致编译不通过。
在 Go 语言中,字典的查找功能设计得比较精巧,要从字典中查找一个特定的键对应的值,可以通过下面的代码来实现:
value, ok := testMap[“one”]
if ok { // 找到了
// 处理找到的value
}
从字典中查找指定键时,会返回两个值,第一个是真正返回的键值,第二个是是否找到的标识,判断是否在字典中成功找到指定的键,不需要检查取到的值是否为 nil,只需查看第二个返回值 ok,这是一个布尔值,如果查找成功,返回 true,否则返回 false,配合 := 操作符,让你的代码没有多余成分,看起来非常清晰易懂。
delete(testMap, “four”)
上面的代码将会从 testMap 中删除键为「four」的键值对。如果「four」这个键不存在或者字典尚未初始化,这个调用也不会有什么副作用。
““““““““““`
keys := make([]string, 0)
for k, _ := range testMap {
keys = append(keys, k)
}
sort.Strings(keys) // 对键进行排序
fmt.Println(“Sorted map by key:”)
for _, k := range keys {
fmt.Println(k, testMap[k])
}
上述代码打印结果是:
Sorted map by key:
one 1
three 3
two 2
“““““““
我们来看一个简单的示例:
a := 100
var ptr *int // 声明指针类型
ptr = &a // 初始化指针类型值为变量 a
fmt.Println(ptr)
fmt.Println(*ptr)
上面代码中的 ptr 就是一个指针类型,表示指向存储 int 类型值的指针。ptr 本身是一个内存地址值,所以需要通过内存地址进行赋值(通过 &a 可以获取变量 a 所在的内存地址),赋值之后,可以通过 *ptr 获取指针指向内存地址存储的变量值(我们通常将这种引用称作「间接引用」),所以上述代码打印结果是:
0xc0000a2000
100
每次打印的 ptr 值可能不一样,因为存储变量 a 的内存地址在变动,不同操作系统打印的结果也不相同
Go 语言之所以引入指针类型,主要基于两点考虑,一个是为程序员提供操作变量对应内存数据结构的能力;另一个是为了提高程序的性能(指针可以直接指向某个变量值的内存地址,可以极大节省内存空间,操作效率也更高),这在系统编程、操作系统或者网络应用中是不容忽视的因素。
“““““““`
指针在 Go 语言中有两个典型的使用场景:
类型指针
切片
作为类型指针时,允许对这个指针类型数据指向的内存地址存储值进行修改,传递数据时如果使用指针则无须拷贝数据从而节省内存空间,此外和 C 语言中的指针不同,Go 语言中的类型指针不能进行偏移和运算,因此更为安全。
切片类型我们前面已经介绍过,由指向数组起始元素的指针、元素数量和容量组成,所以切片与数组不同,是引用类型,而非值类型。
当指针被声明后,没有指向任何变量内存地址时,它的零值是 nil,然后我们可以通过在给定变量前加上取地址符 & 获取该变量对应的内存地址,再将其赋值给声明的指针类型,这样,就完成对指针类型的初始化了,接下来我们可以通过在指针类型前加上间接引用符 * 获取指针指向内存空间存储的变量值。
当然,和所有其他 Go 数据类型一样,我们也可以通过 := 对指针类型进行初始化:
a := 100
ptr := &a
fmt.Printf(“%p\n”, ptr)
fmt.Printf(“%d\n”, *ptr)
底层会自动判断指针的类型,在格式化输出时,可以通过 %p 来标识指针类型。
此外,还可以通过内置函数 new 声明指针:
ptr := new(int)
*ptr = 100
“““““““`
ptr = &a
&a 可以获取变量 a 所在的内存地址
*ptr 获取指针指向内存地址存储的变量值
& 取变量地址, * 通过指针访问目标对象.
指针就是地址, 指针变量就是存储地址的变量.
*p 称为 解引用 或者 间接引用.
不管是 x 还是 *p , 我们操作的都是同一个空间.
和函数体外声明的变量一样,以大写字母开头的常量在包外可见(类似于 public 修饰的类属性),比如上面介绍的 Pi、Sunday 等,而以小写字母开头的常量只能在包内访问(类似于通过 protected 修饰的类属性),比如 zero、numberOfDays 等,后面在介绍包的可见性时还会详细介绍。函数体内声明的常量只能在函数体内生效.
在实际开发中,应该尽可能地使用 float64 类型,因为 math 包中所有有关数学运算的函数都会要求接收这个类型。
浮点数支持通过算术运算符进行四则运算,也支持通过比较运算符进行比较(前提是运算符两边的操作数类型一致),但是涉及到相等的比较除外,因为我们上面提到,看起来相等的两个十进制浮点数,在底层转化为二进制时会丢失精度,因此不能被表象蒙蔽。
如果一定要判断相等,下面是一种替代的解决方案:
p := 0.00001
// 判断 floatValue1 与 floatValue2 是否相等
if math.Dim(float64(floatValue1), floatValue2) < p { fmt.Println("floatValue1 和 floatValue2 相等") } 可以看到,我们的解决方案是一种近似判断,通过一个可以接受的最小误差值 p,约定如果两个浮点数的差值在此精度的误差范围之内,则判定这两个浮点数相等。这个解决方案也是其他语言判断浮点数相等所采用的通用方案。 数组是值类型(关于值类型和引用类型,后面在 Go 类型系统中会详细介绍),这意味着作为参数传递到函数时,传递的是数组的值拷贝,也就是说,会先将数组拷贝给形参,然后在函数体中引用的是形参而不是原来的数组,当我们在函数中对数组元素进行修改时,并不会影响原来的数组,这种机制带来的另一个负面影响是当数组很大时,值拷贝会降低程序性能。 综合以上因素,我们迫切需要一个引用类型的、支持动态添加元素的新「数组」类型,这就是下篇教程将要介绍的切片类型,实际上,我们在 Go 语言中很少使用数组,大多数时候会使用切片取代它。 ``````````` mySlice1 := make([]int, 5) mySlice2 := make([]int, 5, 10) mySlice3 := []int{1, 2, 3, 4, 5} fmt.Println(len(q2)) // 3 fmt.Println(cap(q2)) // 9 切片比数组更强大之处在于支持动态增加元素,甚至可以在容量不足的情况下自动扩容。在切片类型中,元素个数和实际可分配的存储空间是两个不同的值,元素的个数即切片的实际长度,而可分配的存储空间就是切片的容量。 对于基于数组和切片创建的切片而言,默认容量是从切片起始索引到对应底层数组的结尾索引; 对于通过内置 make 函数创建的切片而言,在没有指定容量参数的情况下,默认容量和切片长度一致。 newSlice := append(oldSlice, 1, 2, 3) appendSlice := []int{1, 2, 3, 4, 5} newSlice := append(oldSlice, appendSlice...) // 注意末尾的 ... 不能省略 如果追加的元素个数超出 oldSlice 的默认容量,则底层会自动进行扩容: newSlice := append(oldSlice, 1, 2, 3, 4, 5, 6) fmt.Println(newSlice) fmt.Println(len(newSlice)) fmt.Println(cap(newSlice)) 此时 newSlice 的长度变成了 11,容量变成了 20,需要注意的是 append() 函数并不会改变原来的切片,而是会生成一个容量更大的切片,然后把原有的元素和新元素一并拷贝到新切片中。 默认情况下,扩容后新切片的容量将会是原切片容量的 2 倍,如果还不足以容纳新元素,则按照同样的操作继续扩容,直到新容量不小于原长度与要追加的元素数量之和。但是,当原切片的长度大于或等于 1024 时,Go 语言将会以原容量的 1.25 倍作为新容量的基准。 因此,如果事先能预估切片的容量并在初始化时合理地设置容量值,可以大幅降低切片内部重新分配内存和搬送内存块的操作次数,从而提高程序性能。 要解决这个问题,可以怎么做: slice1 := make([]int, 4) slice2 := slice1[1:3] slice1 = append(slice1, 0) slice1[1] = 2 slice2[1] = 6 fmt.Println("slice1:", slice1) fmt.Println("slice2:", slice2) 打印结果如下: slice1: [0 2 0 0 0] slice2: [0 6] 可以看到,虽然 slice2 是基于 slice1 创建的,但是修改 slice2 不会再同步到 slice1,因为 append 函数会重新分配新的内存,然后将结果赋值给 slice1,这样一来,slice2 会和老的 slice1 共享同一个底层数组内存,不再和新的 slice1 共享内存,也就不存在数据共享问题了。 但是这里有个需要注意的地方,就是一定要重新分配内存空间,如果没有重新分配,依然存在数据共享问题: slice1 := make([]int, 4, 5) slice2 := slice1[1:3] slice1 = append(slice1, 0) slice1[1] = 2 slice2[1] = 6 fmt.Println("slice1:", slice1) fmt.Println("slice2:", slice2) 打印结果如下: slice1: [0 2 6 0 0] slice2: [2 6] 可以看到这里就发生了数据共享问题,因为初始化的容量是 5,比长度大,执行append 的时候没有进行扩容,也就不存在重新分配内存操作。 ··············
前面我们提到 new 函数作用于值类型,仅分配内存空间,返回的是指针,make 函数作用于引用类型,除了分配内存空间,还会对对应类型进行初始化,返回的是初始值。在 Go 语言中,引用类型包括切片(slice)、字典(map)和管道(channel),其它都是值类型。
隐式间接引用
·················
切片就像数组的引用
切片并不存储任何数据,它只是描述了底层数组中的一段。
更改切片的元素会修改其底层数组中对应的元素。
与它共享底层数组的切片都会观测到这些修改
···················
package main
import “fmt”
// 返回一个“返回int的函数”
func fibonacci() func() int {
}
func main() {
f := fibonacci()
for i := 0; i < 10; i++ { fmt.Println(f()) } } ···················· func(ch chan int){ ch <- ACK }(reply_chan)//花括号后直接跟参数列表表示函数调用 ·················· new 函数作用于值类型,仅分配内存空间,返回的是指针,make 函数作用于引用类型,除了分配内存空间,还会对对应类型进行初始化,返回的是初始值。在 Go 语言中,引用类型包括切片(slice)、字典(map)和管道(channel),其它都是值类型。 ```````````````` p1 := new(int) // 返回 int 类型指针,相当于 var p1 *int p2 := new(string) // 返回 string 类型指针 p3 := new([3]int) // 返回数组类型指针,数组长度是 3 type Student struct { id int name string grade string } p4 := new(Student) // 返回对象类型指针 s1 := make([]int, 3) // 返回初始化后的切片类型值,即 []int{0, 0, 0} m1 := make(map[string]int, 2) // 返回初始化的字典类型值,即散列化的 map 结构 ```````````````````
自执行函数,就是在花括号后面加上一对圆括号,例如
func () {
sum := 0
for i := 0; i <= 10; i++ { sum+=1 } fmt.Println(sum) }()
func main(){
d := func(data1 string) {
fmt.Println(data1)
}
d(“I’m an anonymous function.”)
}
/*
输出结果:
I’m an anonymous function.
我们来看一个简单的示例:
a := 100
var ptr *int // 声明指针类型
ptr = &a // 初始化指针类型值为变量 a
fmt.Println(ptr)
fmt.Println(*ptr)
上面代码中的 ptr 就是一个指针类型,表示指向存储 int 类型值的指针。ptr 本身是一个内存地址值,所以需要通过内存地址进行赋值(通过 &a 可以获取变量 a 所在的内存地址),赋值之后,可以通过 *ptr 获取指针指向内存地址存储的变量值(我们通常将这种引用称作「间接引用」),所以上述代码打印结果是:
获取指针(&运算符)和获取对象(*运算符)的运算
Go 语言自带指针隐式解引用 :对于一些复杂类型的指针, 如果要访问成员变量时候需要写成类似*p.field的形式时,只需要p.field即可访问相应的成员。
表达式 new(Type) 和 &Type{}是等价的。
很多编程语言都支持递归函数,所谓递归函数指的是在函数内部调用函数自身的函数
可以看到,尾递归优化版递归函数性能要优于内存缓存技术优化版,并且不需要借助额外的内存空间保存中间结果,因此从性能角度看是更好的选择,就是可读性差一些,理解起来有些困难。
申请方式 内存大小 使用效率 存储内容
栈(stack) 自动申请释放 小 高效
堆(heap) 手动申请释放 大 缓慢
栈具有先进后出,后进先出的特性,数据连续存储,操作简单,使用方便,无需管理。
大部分芯片都对栈提供芯片级别的硬件支持,只需要移动指针就可以快速实现内存的分配和回收。比如局部变量使用栈内存,从而减少不必要的内存分配管理。
栈创建和删除的时间复杂度是O(1),速度快。
栈的缺点是不利于管理大内存,栈中的数据大小和生存周期都是确定的,缺乏灵活性。
堆内存的管理机制相对复杂,有一套相应的分配策略,防止大量小碎片出现,同时加快查找。
堆用于动态创建分配内存,创建和删除节点的时间复杂度是O(logn)。
堆的回收机制也复杂很多,根据内存大小不同,数据生命周期不同,采用相应的回收机制,涉及操作系统的堆管理。
因为堆内存的管理和申请相对复杂,更消耗系统资源,通常生命周期更长使用范围更广的全局变量使用堆内存。
1、栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
2、堆区(heap) — 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表,呵呵。
3、全局区(静态区)(static)—,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 – 程序结束后有系统释放
4、文字常量区 —常量字符串就是放在这里的。 程序结束后由系统释放
5、程序代码区—存放函数体的二进制代码。
值类型:int、float、bool和string这些类型都属于值类型,使用这些类型的变量直接指向存在内存中的值,值类型的变量的值存储在栈中。
能够通过 make() 函数创建的都是引用类型
a := []int
b = a
会发现,好像b和a指向的是同一个数组,事实确实如此。 go中所有的赋值都是值传递,而slice的赋值,也是对slice对象的一次拷贝,也就是说a和b是不同的slice对象,但是他们指向同一个数组
同理map也是如此,就不多讲来。
值类型:基本数据类型int系列,float系列,bool, string,数组和结构体struct
引用类型:指针、slice切片、map、管道chan、interface
两者区别:
(1)内存分配上:值类型内存通常在栈中分配,引用类型通常在堆中分配
(2)存储值上:值类型直接存储值,引用类型存储地址,地址对应的空间才是真正的值,当没有任何变量引用这个地址时,改地址对应的数据空间就会成为一个垃圾,由GC来回收
函数传参
无论是值类型还是引用类型,传递的都是这个变量存储值的一个副本
func f(i int, p *int){
// i是原始变量值的一个副本,i存储的值与原始变量值相等
// 操作i=1并不会使原始变量值变成1
// p是原始变量值的一个副本,它们存储了相同的指针,指向同一个地址
// 操作 *p = 1 会使原始变量指向的值变成1
// 操作 p = &i 会使p指向i,但是对原始变量没有影响
}
值类型变量和引用类型变量
值类型变量a,值为1,存储变量a的内存地址为0xc000092000
指针类型 &a,某个变量的内存地址,即a的地址0xc000092000。
引用类型p,某个变量的地址的别名。
值为某个变量的内存地址(p的值为变量a的内存地址0xc000092000)
其本身作为一个变量也有自己的存储内存地址0xc00008c010。
指针类型的变量,零值都是nil。
值类型的变量,零值是其所在类型的零值。
int32类型的零值是0
string类型的零值是””
bool类型的零值是false
符合结构struct类型的零值是其每个成员的零值的组合
指针类型的变量,需要初始化才能使用。(slice是一个特例,slice的零值是nil,但是可以直接append)
值类型的变量,不用初始化,可以直接使用
make返回的是对象。
对值类型对象的更改,不会影响原始对象的值
对引用类型对象的更改,会影响原始对象的值
new返回的是对象的指针,对指针所在对象的更改,会影响指针指向的原始对象的值。
在 GetXXX 方法中,由于不需要对类的成员变量进行修改,所以不需要传入指针,而 SetXXX 方法需要在函数内部修改成员变量的值,并且该修改要作用到该函数作用域以外,所以需要传入指针类型(结构体是值类型,不是引用类型,所以需要显式传入指针)。
就是一个自定义数据类型的方法集合中仅会包含它的所有「值方法」,而该类型对应的指针类型包含的方法集合才囊括了该类型的所有方法,包括所有「值方法」和「指针方法」,指针方法可以修改所属类型的属性值,而值方法则不能
golang的核心思想是: 不要通过共享内存来通信,我们应该通过通信来共享内存 。
channel主要适用于数据流动,mutex更加适合于数据稳定的场景
channel 成本原高于 Mutex ,因为channel内部有 Mutex
type T struct {
t int
}
func (t T) NewT(tValue int) T {
return T{t: tValue}
}
func main() {
t := T.NewT(T{},999)
fmt.Println(t)
}
输出 {999}
““““““““““““`
调用 type.method 实际上就是把 method 的 receiver 暴露为参数了
一般就这两种:
1. type.method(type{},…)
2. (*type).method(new(type),…)
看 method 怎么的定义了
闭包就是函数返回一个匿名函数
和切片一样,map是Go语言提供的重要数据类型,也是Gopher日常编码中最常使用的类型之一。通过本条的学习我们掌握了map的基本操作和运行时实现原理,并且我们在日常使用map的场合要把握住下面几个要点:
不要依赖map的元素遍历顺序;
map不是线程安全的,不支持并发写;
不要尝试获取map中元素(value)的地址;
尽量使用cap参数创建map,以提升map平均访问性能,减少频繁扩容带来的不必要损耗。
map类型不支持“零值可用”,未显式赋初值的map类型变量的零值为nil。对处于零值状态的map变量进行操作将会导致运行时panic
根据string在运行时的表示可以得到这样一个结论:直接将string类型通过函数/方法参数传入也不会有太多的损耗,因为传入的仅仅是一个“描述符”
直接传入string与传入string指针两者的基准测试结果几乎一模一样,因此Gopher大可放心地直接使用string作为函数/方法参数类型
在能预估出最终字符串长度的情况下,使用预初始化的strings.Builder连接构建字符串效率最高;strings.Join连接构建字符串的平均性能最稳定,如果输入的多个字符串是以[]string承载的,那么strings.Join也是不错的选择;使用操作符连接的方式最直观、最自然,在编译器知晓欲连接的字符串个数的情况下,使用此种方式可以得到编译器的优化处理;fmt.Sprintf虽然效率不高,但也不是一无是处,如果是由多种不同类型变量来构建特定格式的字符串,那么这种方式还是最适合的。
在本条中,我们了解到Go语言内置了string类型,统一了对字符串的抽象,并且为string类型提供了强大的内置操作支持,包括基于+/+=的字符串连接操作,基于==、!=、>、<等的比较操作,O(1)复杂度的长度获取操作,对非ASCII字符提供原生支持,对string类型与slice类型的相互转换提供优化等。此外,Go语言还在标准库中提供了strings和strconv包,可以辅助Gopher对string类型数据进行更多高级操作。鉴于篇幅有限,这里就不赘述了,大家可以自行查阅以上两个包的使用说明文档(关于strings包中的操作,在后文中有详细说明)。
切片是Go语言提供的重要数据类型,也是Gopher日常编码中最常使用的类型之一。
切片是数组的描述符,在大多数场合替代了数组,并减少了数组指针作为函数参数的使用。
append在切片上的运用让切片类型部分支持了“零值可用”的理念,并且append对切片的动态扩容将Gopher从手工管理底层存储的工作中解放了出来。
在可以预估出元素容量的前提下,使用cap参数创建切片可以提升append的平均操作性能,减少或消除因动态扩容带来的性能损耗。
所有
整型类型:0
浮点类型:0.0
布尔类型:false
字符串类型:””
指针、interface、切片(slice)、channel、map、function:nil
切片
s := make([]T, len, cap)
map
const mapSize = 10000
m := make(map[int]int, mapSize)
无类型常量消除烦恼,简化代码
所有常量表达式的求值计算都可以在编译期而不是在运行期完成,这样既可以减少运行时的工作,也能方便编译器进行编译优化。当操作数是常量时,在编译时也能发现一些运行时的错误,例如整数除零、字符串索引越界等。无类型常量是Go语言推荐的实践,它拥有和字面值一样的灵活特性,可以直接用于更多的表达式而不需要进行显式类型转换,从而简化了代码编写。此外,按照Go官方语言规范[2]的描述,数值型无类型常量可以提供比基础类型更高精度的算术运算,至少有256 bit的运算精度。
使用一致的变量声明形式
函数直接或间接调用函数本身,则该函数称为递归函数。使用递归函数时经常会遇到的一个重要问题就是栈溢出:一般出现在大量的递归调用导致的内存分配耗尽。
Go语言中函数可以作为其它函数的参数进行传递,然后在其它函数内调用执行,一般称之为回调
函数值字面量是一种表达式,它的值被称为匿名函数。从形式上看当我们不给函数起名字的时候,可以使用匿名函数,也可以直接对匿名函数进行调用,注意匿名函数的最后面加上了括号并填入了参数值,如果没有参数,也需要加上空括号,代表直接调用:
匿名函数同样也被称之为闭包。
闭包可被允许调用定义在其环境下的变量,可以访问它们所在的外部函数中声明的所有局部变量、参数和声明的其他内部函数。闭包继承了函数所声明时的作用域,作用域内的变量都被共享到闭包的环境中,因此这些变量可以在闭包中被操作,直到被销毁。也可以理解为内层函数引用了外层函数中的变量或称为引用了自由变量。
实质上看,闭包是由函数及其相关引用环境组合而成的实体(即:闭包=函数+引用环境)。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。由闭包的实质含义,我们可以推论:闭包获取捕获变量相当于引用传递,而非值传递;对于闭包函数捕获的常量和变量,无论闭包何时何处被调用,闭包都可以使用这些常量和变量,而不用关心它们表面上的作用域。
换句话说闭包函数可以访问不是它自己内部的变量(这个变量在其它作用域内声明),且这个变量是未赋值的,它在闭包里面赋值。