在Ubuntu上一键安装Docker
1 | apt-get update \ |
1 | apt-get update \ |
1 | func main() { |
golang 100 true
在同一个 const group 中,如果常量定义与前一行的定义一致,则可以省略类型和值。编译时,会按照前一行的定义自动补全。即等价于
1 | func main() { |
1 | func main() { |
编译失败:cannot use M (type int32) as type int in assignment
Go 语言中,常量分为无类型常量和有类型常量两种,const N = 100
,属于无类型常量,赋值给其他变量时,如果字面量能够转换为对应类型的变量,则赋值成功,例如,var x int = N
。但是对于有类型的常量 const M int32 = 100
,赋值给其他变量时,需要类型匹配才能成功,所以显示地类型转换:
1 | var y int = int(M) |
1 | func main() { |
-128
int8 能表示的数字的范围是 [-2^7, 2^7-1],即 [-128, 127]。-128 是无类型常量,转换为 int8,再除以变量 -1,结果为 128,常量除以变量,结果是一个变量。变量转换时允许溢出,符号位变为1,转为补码后恰好等于 -128。
对于有符号整型,最高位是是符号位,计算机用补码表示负数。补码 = 原码取反加一。
例如:
1 | -1 : 11111111 |
1 | func main() { |
编译失败:constant 128 overflows int8
-128 和 a 都是常量,在编译时求值,-128 / a = 128,两个常量相除,结果也是一个常量,常量类型转换时不允许溢出,因而编译失败。
1 | func main() { |
1 err
:=
表示声明并赋值,=
表示仅赋值。
变量的作用域是大括号,因此在第一个 if 语句 if err == nil
内部重新声明且赋值了与外部变量同名的局部变量 err。对该局部变量的赋值不会影响到外部的 err。因此第二个 if 语句 if err != nil
不成立。所以只打印了 1 err
。
1 | type T struct{} |
132
defer 延迟调用时,需要保存函数指针和参数,因此链式调用的情况下,除了最后一个函数/方法外的函数/方法都会在调用时直接执行。也就是说 t.f(1)
直接执行,然后执行 fmt.Print(3)
,最后函数返回时再执行 .f(2)
,因此输出是 132。
1 | func f(n int) { |
1
打印 1 而不是 101。defer 语句执行时,会将需要延迟调用的函数和参数保存起来,也就是说,执行到 defer 时,参数 n(此时等于1) 已经被保存了。因此后面对 n 的改动并不会影响延迟函数调用的结果。
1 | func main() { |
101
匿名函数没有通过传参的方式将 n 传入,因此匿名函数内的 n 和函数外部的 n 是同一个,延迟执行时,已经被改变为 101。
1 | func main() { |
1 | 101 |
先打印 101,再打印 1。defer 的作用域是函数,而不是代码块,因此 if 语句退出时,defer 不会执行,而是等 101 打印后,整个函数返回时,才会执行。
对于无缓冲的 channel,发送方将阻塞该信道,直到接收方从该信道接收到数据为止,而接收方也将阻塞该信道,直到发送方将数据发送到该信道中为止。
对于有缓存的 channel,发送方在没有空插槽(缓冲区使用完)的情况下阻塞,而接收方在信道为空的情况下阻塞。
例如:
1 | func main() { |
1 | func main() { |
协程泄露是指协程创建后,长时间得不到释放,并且还在不断地创建新的协程,最终导致内存耗尽,程序崩溃。常见的导致协程泄露的场景有以下几种:
这个例子中,每执行一次 query,则启动1000个协程向信道 ch 发送数字 0,但只接收了一次,导致 999 个协程被阻塞,不能退出。
1 | func query() int { |
那同样的,如果启动 1000 个协程接收信道的信息,但信道并不会发送那么多次的信息,也会导致接收协程被阻塞,不能退出。
两个或两个以上的协程在执行过程中,由于竞争资源或者由于彼此通信而造成阻塞,这种情况下,也会导致协程被阻塞,不能退出。
这个例子中,为了避免网络等问题,采用了无限重试的方式,发送 HTTP 请求,直到获取到数据。那如果 HTTP 服务宕机,永远不可达,导致协程不能退出,发生泄漏。
1 | func request(url string, wg *sync.WaitGroup) { |
The GOMAXPROCS variable limits the number of operating system threads that can execute user-level Go code simultaneously. There is no limit to the number of threads that can be blocked in system calls on behalf of Go code; those do not count against the GOMAXPROCS limit.
可以使用环境变量 GOMAXPROCS
或 runtime.GOMAXPROCS(num int)
设置,例如:
1 | runtime.GOMAXPROCS(1) // 限制同时执行Go代码的操作系统线程数为 1 |
从官方文档的解释可以看到,GOMAXPROCS
限制的是同时执行用户态 Go 代码的操作系统线程的数量,但是对于被系统调用阻塞的线程数量是没有限制的。GOMAXPROCS
的默认值等于 CPU 的逻辑核数,同一时间,一个核只能绑定一个线程,然后运行被调度的协程。因此对于 CPU 密集型的任务,若该值过大,例如设置为 CPU 逻辑核数的 2 倍,会增加线程切换的开销,降低性能。对于 I/O 密集型应用,适当地调大该值,可以提高 I/O 吞吐率。
init()
函数是 Go 程序初始化的一部分。Go 程序初始化先于 main 函数,由 runtime 初始化每个导入的包,初始化顺序不是按照从上到下的导入顺序,而是按照解析的依赖关系,没有依赖的包最先初始化。
每个包首先初始化包作用域的常量和变量(常量优先于变量),然后执行包的 init()
函数。同一个包,甚至是同一个源文件可以有多个 init()
函数。init()
函数没有入参和返回值,不能被其他函数调用,同一个包内多个 init()
函数的执行顺序不作保证。
一句话总结: import –> const –> var –> init()
–> main()
示例:
1 | package main |
由编译器决定。Go 语言编译器会自动决定把一个变量放在栈还是放在堆,编译器会做逃逸分析(escape analysis),当发现变量的作用域没有超出函数范围,就可以在栈上,反之则必须分配在堆上。
1 | func foo() *int { |
foo()
函数中,如果 v 分配在栈上,foo 函数返回时,&v
就不存在了,但是这段函数是能够正常运行的。Go 编译器发现 v 的引用脱离了 foo 的作用域,会将其分配在堆上。因此,main 函数中仍能够正常访问该值。
Go 语言中,interface 的内部实现包含了 2 个字段,类型 T
和 值 V
,interface 可以使用 ==
或 !=
比较。2 个 interface 相等有以下 2 种情况
看下面的例子:
1 | type Stu struct { |
stu1
和 stu2
对应的类型是 *Stu
,值是 Stu 结构体的地址,两个地址不同,因此结果为 false。stu3
和 stu4
对应的类型是 Stu
,值是 Stu 结构体,且各字段相等,因此结果为 true。
可能。
接口(interface) 是对非接口值(例如指针,struct等)的封装,内部实现包含 2 个字段,类型 T
和 值 V
。一个接口等于 nil,当且仅当 T 和 V 处于 unset 状态(T=nil,V is unset)。
1 | func main() { |
上面这个例子中,将一个 nil 非接口值 p 赋值给接口 i,此时,i 的内部字段为(T=*int, V=nil)
,i 与 p 作比较时,将 p 转换为接口后再比较,因此 i == p
,p 与 nil 比较,直接比较值,所以 p == nil
。
但是当 i 与 nil 比较时,会将 nil 转换为接口 (T=nil, V=nil)
,与i (T=*int, V=nil)
不相等,因此 i != nil
。因此 V 为 nil ,但 T 不为 nil 的接口不等于 nil。
最常见的垃圾回收算法有标记清除(Mark-Sweep) 和引用计数(Reference Count),Go 语言采用的是标记清除算法。并在此基础上使用了三色标记法和写屏障技术,提高了效率。
标记清除收集器是跟踪式垃圾收集器,其执行过程可以分成标记(Mark)和清除(Sweep)两个阶段:
标记清除算法的一大问题是在标记期间,需要暂停程序(Stop the world,STW),标记结束之后,用户程序才可以继续执行。为了能够异步执行,减少 STW 的时间,Go 语言采用了三色标记法。
三色标记算法将程序中的对象分成白色、黑色和灰色三类。
标记开始时,所有对象加入白色集合(这一步需 STW )。首先将根对象标记为灰色,加入灰色集合,垃圾搜集器取出一个灰色对象,将其标记为黑色,并将其指向的对象标记为灰色,加入灰色集合。重复这个过程,直到灰色集合为空为止,标记阶段结束。那么白色对象即可需要清理的对象,而黑色对象均为根可达的对象,不能被清理。
三色标记法因为多了一个白色的状态来存放不确定对象,所以后续的标记阶段可以并发地执行。当然并发执行的代价是可能会造成一些遗漏,因为那些早先被标记为黑色的对象可能目前已经是不可达的了。所以三色标记法是一个 false negative(假阴性)的算法。
三色标记法并发执行仍存在一个问题,即在 GC 过程中,对象指针发生了改变。比如下面的例子:
1 | A (黑) -> B (灰) -> C (白) -> D (白) |
正常情况下,D 对象最终会被标记为黑色,不应被回收。但在标记和用户程序并发执行过程中,用户程序删除了 C 对 D 的引用,而 A 获得了 D 的引用。标记继续进行,D 就没有机会被标记为黑色了(A 已经处理过,这一轮不会再被处理)。
1 | A (黑) -> B (灰) -> C (白) |
为了解决这个问题,Go 使用了内存屏障技术,它是在用户程序读取对象、创建新对象以及更新对象指针时执行的一段代码,类似于一个钩子。垃圾收集器使用了写屏障(Write Barrier)技术,当对象新增或更新时,会将其着色为灰色。这样即使与用户程序并发执行,对象的引用发生改变时,垃圾收集器也能正确处理了。
一次完整的 GC 分为四个阶段:
这在 Go 中是安全的,Go 编译器将会对每个局部变量进行逃逸分析。如果发现局部变量的作用域超出该函数,则不会将内存分配在栈上,而是分配在堆上。
*T
的方法吗?反过来呢?*T
类型声明的方法,但是仅当此T的值是可寻址(addressable) 的情况下。编译器在调用指针属主方法前,会自动取此T值的地址。因为不是任何T值都是可寻址的,所以并非任何T值都能够调用为类型*T
声明的方法。*T
类型的值可以调用为类型T声明的方法,这是因为解引用指针总是合法的。事实上,你可以认为对于每一个为类型 T 声明的方法,编译器都会为类型*T
自动隐式声明一个同名和同签名的方法。哪些值是不可寻址的呢?
举一个例子,定义类型 T,并为类型 *T
声明一个方法 hello()
,变量 t1 可以调用该方法,但是常量 t2 调用该方法时,会产生编译错误。
1 | type T string |
=
和 :=
的区别?:=
声明+赋值
=
仅赋值
1 | var foo int |
指针用来保存变量的地址。
例如
1 | var x = 5 |
*
运算符,也称为解引用运算符,用于访问地址中的值。&
运算符,也称为地址运算符,用于返回变量的地址。允许
1 | func swap(x, y string) (string, string) { |
Go 没有异常类型,只有错误类型(Error),通常使用返回值来表示异常状态。
1 | f, err := os.Open("test.txt") |
Goroutine 是与其他函数或方法同时运行的函数或方法。 Goroutines 可以被认为是轻量级的线程。 与线程相比,创建 Goroutine 的开销很小。 Go应用程序同时运行数千个 Goroutine 是非常常见的做法。
Go 语言中,字符串是只读的,也就意味着每次修改操作都会创建一个新的字符串。如果需要拼接多次,应使用 strings.Builder
,最小化内存拷贝次数。
1 | var str strings.Builder |
ASCII 码只需要 7 bit 就可以完整地表示,但只能表示英文字母在内的128个字符,为了表示世界上大部分的文字系统,发明了 Unicode, 它是ASCII的超集,包含世界上书写系统中存在的所有字符,并为每个代码分配一个标准编号(称为Unicode CodePoint),在 Go 语言中称之为 rune,是 int32 类型的别名。
Go 语言中,字符串的底层表示是 byte (8 bit) 序列,而非 rune (32 bit) 序列。例如下面的例子中 语
和 言
使用 UTF-8 编码后各占 3 个 byte,因此 len("Go语言")
等于 8,当然我们也可以将字符串转换为 rune 序列。
1 | fmt.Println(len("Go语言")) // 8 |
1 | if val, ok := dict["foo"]; ok { |
dict["foo"]
有 2 个返回值,val 和 ok,如果 ok 等于 true
,则说明 dict 包含 key "foo"
,val 将被赋予 "foo"
对应的值。
Go 语言不支持可选参数(python 支持),也不支持方法重载(java支持)。
例如:
1 | func test() int { |
这个例子中,可以看到 defer 的执行顺序:后进先出。但是返回值并没有被修改,这是由于 Go 的返回机制决定的,执行 return 语句后,Go 会创建一个临时变量保存返回值,因此,defer 语句修改了局部变量 i,并没有修改返回值。那如果是有名的返回值呢?
1 | func test() (i int) { |
这个例子中,返回值被修改了。对于有名返回值的函数,执行 return 语句时,并不会再创建临时变量保存,因此,defer 语句修改了 i,即对返回值产生了影响。
1 | a, b := "A", "B" |
tag 可以理解为 struct 字段的注解,可以用来定义字段的一个或多个属性。框架/工具可以通过反射获取到某个字段定义的属性,采取相应的处理方式。tag 丰富了代码的语义,增强了灵活性。
例如:
1 | package main |
这个例子使用 tag 定义了结构体字段与 json 字段的转换关系,Name -> stu_name
, ID -> stu_id
,忽略 Age 字段。很方便地实现了 Go 结构体与不同规范的 json 文本之间的转换。
go 语言中可以使用反射 reflect.DeepEqual(a, b)
判断 a、b 两个切片是否相等,但是通常不推荐这么做,使用反射非常影响性能。
通常采用的方式如下,遍历比较切片中的每一个元素(注意处理越界的情况)。
1 | func StringSliceEqualBCE(a, b []string) bool { |
%v
和 %+v
的区别%v
和 %+v
都可以用来打印 struct 的值,区别在于 %v
仅打印各个字段的值,%+v
还会打印各个字段的名称。
1 | type Stu struct { |
但如果结构体定义了 String()
方法,%v
和 %+v
都会调用 String()
覆盖默认值。
通常使用常量(const) 来表示枚举值。
1 | type StuType int32 |
参考 What is an idiomatic way of representing enums in Go? - StackOverflow
使用空结构体 struct{} 可以节省内存,一般作为占位符使用,表明这里并不需要一个值。
1 | fmt.Println(unsafe.Sizeof(struct{}{})) // 0 |
比如使用 map 表示集合时,只关注 key,value 可以使用 struct{} 作为占位符。如果使用其他类型作为占位符,例如 int,bool,不仅浪费了内存,而且容易引起歧义。
1 | type Set map[string]struct{} |
再比如,使用信道(channel)控制并发时,我们只是需要一个信号,但并不需要传递值,这个时候,也可以使用 struct{} 代替。
1 | func main() { |
再比如,声明只包含方法的结构体。
1 | type Lamp struct{} |
Optuna是一个自动化超参数优化框架,主要用于机器学习项目。它的目标是通过试验的方式找到一个最优的超参数组合,以提高模型的性能。以下是Optuna的基本原理和特性:
搜索空间定义:在Optuna中,用户需要定义一个超参数的搜索空间。这个空间通常是一个高维的空间,每一个维度对应一个超参数。搜索空间可以是离散的也可以是连续的,取决于超参数的性质。
目标函数:用户需要定义一个目标函数,这个函数通常代表模型的某种性能指标,比如准确度或者损失函数。Optuna的目标就是找到一个超参数组合,使得这个目标函数的值最大(或者最小)。
采样策略:Optuna使用了一种特殊的采样策略,叫做Tree-structured Parzen Estimator (TPE)。TPE是一种基于贝叶斯优化的采样方法,它可以有效地搜索高维度和非凸的超参数空间。
剪枝策略:为了加速搜索过程,Optuna还采用了一种剪枝策略。如果在搜索过程中,一个试验的中间结果已经很差,Optuna可以提前停止这个试验,把资源用在更有希望的地方。
并行优化:Optuna还支持分布式的并行优化。如果有多个计算资源可用,Optuna可以在不同的资源上并行进行多个试验,这样可以大大加速搜索过程。
Optuna的优化过程是迭代的。在每次迭代中,它先使用采样策略从搜索空间中选择一个超参数组合,然后使用这个组合运行目标函数,并记录函数的返回值。然后,根据这个返回值和剪枝策略来决定是否继续进行这个试验,或者是否需要调整采样策略。这个过程会一直进行,直到找到一个最优的超参数组合,或者达到预设的试验次数。
下面是一个使用Optuna来优化Scikit-Learn随机森林分类器超参数的例子。这个例子使用的是Iris数据集,我们的目标是找到最优的超参数组合,使得分类器的准确度最高。
1 | import optuna |
在这个例子中:
objective
。这个函数有一个参数trial
,这是Optuna在每次优化试验中传递的对象。我们可以使用trial.suggest_
系列的方法从超参数空间中抽样。study
对象,并调用它的optimize
方法来进行优化。在优化过程中,Optuna会根据目标函数的返回值和剪枝策略来选择最优的超参数组合。1 | import gym |
这段代码主要是用于强化学习(Reinforcement Learning)模型训练和测试的。在这段代码中,首先创建了一个自定义的环境(CustomMountainCarEnv),然后在这个环境中训练一个使用PPO(Proximal Policy Optimization)算法的模型,最后对模型进行了测试。
让我们逐步地详细分析这段代码:
导入依赖库:这段代码中使用了gym、stable_baselines3等库。Gym是一个用于开发和比较强化学习算法的工具库,它包含了许多预定义的环境。stable_baselines3则是一个提供实现了各种强化学习算法的模型的库。
定义自定义环境(CustomMountainCarEnv):这个环境基于Gym库中的’MountainCar-v0’环境,但是修改了其奖励(reward)的计算方式。在新的奖励机制中,奖励与车辆的位置与目标位置(0.5)的距离成反比。
环境向量化和监控:使用DummyVecEnv进行环境向量化,这是与stable_baselines兼容所必需的,它允许模型在训练时采取批量(batch)的动作。而Monitor则是用来追踪训练过程中的一些指标,如奖励等。
模型的训练与加载:如果测试模式(test_mode)为False,就会训练一个新的模型;否则,就会加载一个已有的模型。训练模型时,使用的是PPO算法,这是一种策略优化算法,它可以在策略改变过程中保持一定的稳定性。
评估模型:使用stable_baselines3中的evaluate_policy方法,通过在环境中运行模型来评估其性能。这个过程会运行多次(n_eval_episodes=10),计算得出平均奖励(mean_reward)和奖励的标准差(std_reward)。
测试模型:通过让模型在环境中运行,查看模型的性能。每次环境结束后(done = True),如果步骤数不为200(这是’MountainCar-v0’环境的默认最大步数),就认为这是一个成功的尝试。
最后,关闭环境以释放资源。
总的来说,这段代码实现了在自定义环境中使用PPO模型进行强化学习的全过程,包括环境的定义、模型的训练、评估以及测试。
stable_baselines3
库主要提供了 NormalActionNoise
和 OrnsteinUhlenbeckActionNoise
这两种噪声类,这两种噪声已经覆盖了强化学习中最常见的需求。通常,NormalActionNoise
用于添加独立并且相同分布(i.i.d)的高斯噪声,而 OrnsteinUhlenbeckActionNoise
则是为动作序列提供时间相关性的噪声。
然而,如果这两种类型的噪声无法满足您的需求,您可以自定义噪声生成类。自定义的噪声生成类应该实现 __call__
方法,当每次调用该对象时,它应返回一组新的噪声值。
例如,如果您想使用均匀分布的噪声,可以创建如下的噪声类:
1 | class UniformActionNoise: |
在这个例子中,每当这个对象被调用,它都会从一个均匀分布中抽样一个新的噪声值。注意到 reset
方法在这个例子中并没有实际的功能,但它是必须存在的,因为在模型中可能会在某些时间调用它。如果在你的自定义噪声中,reset
方法有其特定的功能,你可以在里面添加相应的实现。
让我们考虑这样一组数据:[1, 2, 3, 3, 3]。
首先,我们计算平均值 (a):
a = (1+2+3+3+3) / 5 = 2.4
接着,我们计算每个数据点与平均值的差,以及这些差值的平方:
(1-2.4)^2 = 1.96
(2-2.4)^2 = 0.16
(3-2.4)^2 = 0.36
(3-2.4)^2 = 0.36
(3-2.4)^2 = 0.36
然后,我们求这些平方的平均值(即方差):
(1.96 + 0.16 + 0.36 + 0.36 + 0.36) / 4 = 0.8
最后,我们求方差的平方根,得到标准差 (b):
b = sqrt(0.8) ≈ 0.89
所以,这组数据的标准差为0.89,平均值为2.4。平均值与标准差之和为 2.4 + 0.89 = 3.29。
然而,这组数据的最大值为3,显然小于平均值与标准差之和3.29。