图灵已经找到了计算能力的基石,但寻找智慧的基石这项工作,仍然没有见到成功的曙光,世界在等待着下一个“图灵”的出现。
谁会是下一个图灵呢,是奥特曼吗,我们不得而知,GPT的发布如火如荼,吸引了全世界的注意力。
我并不看好目前的AI进展,不管是近十年深度学习的飞速发展,还是Transformer近期的又一次颠覆,这些都不是真正的智能。真正的智能应该不需要训练,具备自主学习能力。就语言能力而言,真正的智能能从零开始自己解析语言,不需要任何数据教他。就像三体中提到的自译解析系统一样。
虽然都声称已经成功开发出了非冯·诺依曼的新型计算机架构,然而就背后对应的非图灵机的新型计算模型而言,仍是毫无头绪。
或许图灵机这个模型就无法实现这样的AI。那么任何冯诺依曼架构的计算机就都无法实现。或许非冯诺依曼架构才有新的可能?当然这都是我的猜想。
伟大的图灵吞食氰化物结束的自己传奇的一生,当时的社会是不够包容的(同性恋难以理解,但我尊重他的选择)。
“伊莉莎效应”(ELIZA Effect),这个词的意思是说人可以过度解读机器的结果,读出原本不具有的意义。
gpt4就是是eliza effect的典型代表,短视频创作者们无所不用其极。
冯诺依曼曾推理出一个智能系统应当包含一个自复制系统,而一个自复制系统应该具备两个能力。
首先,它必须能够构建一个组成元素和结构与自己一致的后代系统,然后它需要能够把对自身的描述传递给这个后代系统。冯·诺依曼把这两个部分分别称作“通用构造器”和“描述器”,而描述器中又包含了一个通用图灵机和保存在通用图灵机器能够读取的介质上的描述信息。这样,只要有合适的原料,通用构造器就可以根据描述器的指示,生产出下一台机器,并且把描述的信息也传递给这台新机器。随后,新机器启动,再进入下一个复制循环。
在当时,没有设计出能够进行自我复制的机器,但是遵循冯诺依曼的思路进行自我复制的软件已经存在:DNA双螺旋结构。
]]>Transformer指的是一种架构,未来的NLP(自然语言处理)
都可以使用此架构。Transformer由编码器、解码器(后面会介绍)等组成,他的输入是向量数组,输出是也是向量数组。
注意:Transformer架构的输出是把向量逐个输出的。
Transformer架构最初只用来做机器翻译,例如将英语翻译成德语。在翻译的过程中待翻译语句
作为输入,而翻译结果
作为输出。待翻译语句
中的每一个Token(词)
会被转化为向量作为输入,Transformer的输出向量最终被转化为翻译结果
的Token(词)
。由于Transformer逐一输出结果的特性,翻译结果
是一个Token(词)
一个Token(词)
逐渐输出的。
Transformer架构不仅仅只可以用与机器翻译,Transformer现在也用于GPT中,就是GPT的最后一个字母T所代表的含义。由于Transformer逐一输出结果的特性,GPT也是一个Token(词)
一个Token(词)
逐渐输出的。
注意力机制源自做翻译时的技巧,当我们翻译某个词的时候,我们应该注意这个词。例如我们如果要翻译以下句子为中文:
1 | Hello World ! |
假设他的标准答案是:你好,世界!
当我们写下中文你好
的时候,我们需要注意单词Hello
,因为这个单词将被翻译成你好
,当我们写下中文世界
的时候,我们需要注意单词World
。注意力机制就试图让机器明白以上道理。
更加严谨一点,在训练时,用公式表示即为:假设已知输入为向量数组$K$包含向量$K_1$,$K_2$,$K_3$等。假设预期输出为向量$V$包含向量$V_1$,$V_2$,$V_3$等,已知注意力为$A$,$A_{ij}$代表$V_i$的计算中,$K_{j}$所占的权重。
那么便有了计算公式$V_i=F(f_{i1},f_{i2},f_{i3}…)$,$f_{ij}=f(A_{ij},K_{j})$, 注意其中的$F$函数和$f$函数,如何设定都可以。
在测试时,假设一个测试数据为向量$Q$包含$Q_1Q_2Q_3$, 按照$V$的计算公式,其中只有$A$需要变更,因为$A_{ij}$代表$V_i$(而不是$Q_i$)的计算中,$K_{j}$所占的权重。这里只需要想办法换成想要的注意力即可,方法有很多,主要围绕$Q$和$V$的相似关系来计算,这里不做展开。下文介绍一种使用自注意力机制的方案来计算相似关系。
自注意力机制源于另一个技巧,当我们翻译某个词的时候,我们应该注意另一个词。例如:
1 | The program has a bug |
当我们翻译bug
的时候,不能把它翻译成虫子
。我们需要注意到前面的单词program
,因此翻译成问题
因此我们将一份训练数据的输入,即当做$K$,又当做$V$,也当做$Q$。于是就可以训练出前文说的注意力$A$
// TODO
由于自注意力机制可学习的参数较少,多头自注意力机制用来扩充自注意力机制的可学习参数。
多头自注意力机制,将输入投影到$N$个不同的空间,得到$N$个输入,分别做自注意力机制,得到$N$个输出,最终通过全连接层投影回来,得到一个输出。
在这个过程中,产生了投影的参数、全连接的参数,可学习参数就多起来了。
自注意力模型,不受位置的影响。考虑一个输入是向量数组,若打乱输入顺序,不影响输出结果,这个是不合适的,可以考虑直接把位置也编码进输入中,方案是使用三角函数给输入向量的每一个维度都加上周期的三角函数值。
为什么是三角函数?这是收到计算机中数字编码的启发。若把计算机中的32位数字的每一个位拿来看,从0到最大值,32位的每一个位都是周期不同的01变换(最低位变换为010101…,次低位是001100110011…,再次为000011110000111100001111…),这其实也是一种三角函数。
// TODO
x
]]>这里立Flag,没人看得到,没人看得到的Flag,就是能实现的Flag。
2023,每天读书1.5小时;
2023,周末每天读书6小时。
2023,每个月一篇技术Blog,一篇文学Blog。
2023,变成精致男孩,哈哈哈。
]]>VPN 和Proxy有什么区别呢,国内的各大平台极力屏蔽了VPN和Proxy这两个关键字,很多人都分不清他们的关系。
VPN全名虚拟专用网络,Proxy全名代理。
其实,VPN工作于操作系统级别,而Proxy工作在应用级别。如果你配置了VPN,那么所有的应用都进入了虚拟的网络;但如果你配置了代理,只有支持代理的那部分应用才可以进入到虚拟的网络,不支持代理的APP,则无法使用虚拟网络。
上面两张图已经完美诠释了VPN和Proxy的核心区别了。
]]>CPU分两类,精简指令集CPU(RISC)和复杂指令集CPU(CISC)。Intel、AMD厂商主要做复杂指令集CPU;IBM,ARM厂商主要做精简指令集CPU。
精简指令集CPU能耗低,性能相对较弱,常用于移动设备;复杂指令集CPU能耗高,性能强,常用于桌面设备。
不同厂商做的CPU,其架构也不尽相同,目前为止,比较多的架构有四种,ARM架构、X86架构、MIPS架构、PowerPC架构。
ARM架构CPU为精简指令集CPU;X86架构CPU为复杂指令集CPU;MIPS架构和PowerPC架构目前用的不太多了。笔者大学期间学习计算机组成原理的时候,学的就是MIPS架构。
]]>1 | docker run -it --rm centos:centos8 |
1 | sed -i 's/mirrorlist/#mirrorlist/g' /etc/yum.repos.d/CentOS-* \ |
1 | yum install git -y |
1 |
1 | yum install clang -y |
1 | ``` |
1 | /usr/sbin/sshd |
很老的一本书,但是依然有阅读价值。
全书分为4个部分,其中比较重要的应该是第二章和第三章,第二章主要讲解客户端(浏览器)如何保障用户的安全,第三章主要讲解服务端如何保障用户安全。
客户端这边容易受到攻击的主要有三个类型。
第一个是XSS攻击,全名跨站脚本攻击,通常指的是黑客通过某些手段,篡改了用户访问的页面,导致页面上执行了一些恶意的脚本。一个简单的例子:在你登陆淘宝之后,黑客篡改了你的淘宝页面(篡改了用户访问的页面),自动帮你购买了一些奇怪的东西(执行了一些恶意的脚本)。
第二个是CSRF攻击,全名跨站点请求伪造,通常指的是黑客通过某些手段,诱导用户访问某钓鱼页面,在该页面上完成了一些恶意的脚本。一个简单的例子: 在一些诱惑下你点开了一个页面(诱导用户访问某钓鱼页面),尽管你之后什么都没有做,但是你的淘宝购物车被恶意清空了(在该页面上完成了一些恶意的脚本)
第三个是ClickJacking攻击,全名点击劫持攻击,通常指的是黑客通过某些手段,诱导用户访问某钓鱼页面,该页面有个按钮,按钮上悬浮了一个透明的iframe,然后引诱你点击该按钮,当你点击该按钮时触发了iframe中的按钮。一个例子是:在一些诱惑下你点开了一个页面,然后在该页面的诱惑下你点击了一个按钮,结果你的淘宝购物车被恶意清空了。
XSS的核心就是篡改用户访问的页面,攻击者是如何来实施篡改的呢?
很多博客都有一些评论区,用户可以在其中输入一些文本,然后这些文本会展示在评论区。
部分评论区直接把用户的文本解析为HTML语言,
但如果用户输入了一些别有用心的文本,其中包含了一些可执行的脚本,然后被浏览器执行了,这就是XSS攻击。
所以实际上XSS的是攻击者利用了系统的漏洞,导致系统没有按照开发者所期望的那样运行导致的。这种情况和SQL注入特别相似。
开发者所需要做的就是不要信任用户的输入,不要执行用户的输入内容即可,对于评论系统,可以只允许用户输入部分带限制的内容,借此来解决这个问题。
CSRF的核心是跨站,很多开发者对跨站请求了解不多,在一些偶然的情况下,允许任何跨域请求来到自己的后台,这其实是非常危险的。
开发者允许任何域的请求进行跨域,结果攻击者自己做了一个网站,在里面通过跨域调用,删除当前用户的所有数据,
这时候如果一个大冤种来到了这个网站,大冤种的数据就全部被删除了。
不要允许不可信任的域发起跨域请求就能解决这个问题。当然有些请求不涉及到跨域,比如GET请求,这种就要求开发者不要把一些重要的敏感度高的请求用GET实现。
点击劫持,多发生于,iframe页面,这个和iframe有关,这里知道原理就行,就是一个透明的iframe在作妖,具体细节笔者不感兴趣。
后面的内容一般般了,不写了
]]>AspectJ官网^4
AspectJ文档^1
pointcuts指的是程序中的某些链接点(某些时机),例如call(void Point.setX(int))
表示:调用类Point
的setX(int)
方法时
pointcuts可以使用与或非表达式(||,&&,!
)连接,比如 call(void Point.setX(int)) || call(void Point.setY(int))
pointcuts可以被定义为变量,如下面代码中的move()
1 | pointcut move(): |
当然pointcuts定义的时候还可以使用通配符,比如call(void Figure.make*(..))
代表Figure
的以make开头且返回值为void的方法(不关心参数)调用的时候。比如call(public * Figure.* (..))
代表Figure
的任何方法(不关心方法名,参数,返回值)调用的时候。
cflow是什么?
pointcuts 指出了一些事件发生的时机,当这些事件真正发生的时候,我们需要advice表示该做些什么。advice如下,advice可以使用before代表在pointcuts发生以前做一些事情,如下
1 | before(): move() { |
使用after代表在pointcuts发生以后做一些事情,如下
1 | after(): move(){ |
after还可以加上修饰符returning和throwing,分别表示在正常返回和在异常返回的情况,如下
1 | after() returning: move(){ |
around表示环绕一个方法
aspect是一个特别的类型,在其中可以定义pointcut和advice,如下
1 | aspect MyAspect{ |
当然aspect兼容java,你也可以定义各种方法,变量
1 | aspect MyAspect{ |
在官网^4下载最新版本的jar包,笔者这里的最新版本是aspectj-1.9.6.jar ,下载以后双击运行进行安装。
AspectJ安装
安装AspectJ插件^2即可
在IDEA的设置中选择AJC编译器,并指定1.2.1中安装的ajctool的位置。
IDEA_Enable_AJC
把1.2.1中安装的lib包放到项目的依赖中。
Add_AJC_Lib.png
下面是一份Helloworld的代码,源程序只输出+号,但是被aspect所拦截,最终输出了Hello + World!
1 | class HelloWorld { |
执行一个特定的方法的时候
1 | execution(void Point.setX(int)) |
例如下面的代码
1 | class HelloWorld { |
被编译为了
1 | // |
代码被直接写入到了hello方法执行的地方。
调用一个特定方法的时候
1 | call(void Point.setX(int)) |
例如下面的代码
1 | class HelloWorld { |
被编译为了
1 | // |
可以看到,是在调用hello方法的前后增加了一些内容。
处理异常的时候
1 | handler(ArrayOutOfBoundsException) |
如下面的代码
1 | class HelloWorld { |
被编译为了
1 | // |
可以发现就是在catch该exception后的第一步操作
当然AspectJ还有很多很多可以定义point cut的关键词,笔者这里就不一一列举了,相见文档[^5]即可
所以AspectJ其实是对Java语法的拓展,通过特定的编译器,给Java带来了更强大的能力。
官方文档^3
Spring支持AspectJ的一个子集,所支持的pointcut如下
Spring AOP supports the following AspectJ pointcut designators (PCD) for use in pointcut expressions:
execution
: For matching method execution join points. This is the primary pointcut designator to use when working with Spring AOP.within
: Limits matching to join points within certain types (the execution of a method declared within a matching type when using Spring AOP).this
: Limits matching to join points (the execution of methods when using Spring AOP) where the bean reference (Spring AOP proxy) is an instance of the given type.target
: Limits matching to join points (the execution of methods when using Spring AOP) where the target object (application object being proxied) is an instance of the given type.args
: Limits matching to join points (the execution of methods when using Spring AOP) where the arguments are instances of the given types.@target
: Limits matching to join points (the execution of methods when using Spring AOP) where the class of the executing object has an annotation of the given type.@args
: Limits matching to join points (the execution of methods when using Spring AOP) where the runtime type of the actual arguments passed have annotations of the given types.@within
: Limits matching to join points within types that have the given annotation (the execution of methods declared in types with the given annotation when using Spring AOP).@annotation
: Limits matching to join points where the subject of the join point (the method being run in Spring AOP) has the given annotation.
spring中的aspectJ,并非使用拓展java语法,而是使用注解来拓展,spring中的aspectj也不是使用的aspectj编译器,而是使用的spring aop来完成代理。但spring也支持通过开关使用原生aspectj[^6]。
aspect的类型用注解@Aspect表示
pointcut字段用@Pointcut表示
advice分别用@Before、@After、@Around来表示
[^5]: Aspect pointcut 文档
[^6]: Spring中使用AspectJ编译器
1 | int main() { |
我们能看到这里输出的两个值相同。
错误1:
很多人认为这里的b就是a,a就是b,a和b的地址是一样的,如下图。
但是笔者要说,其实这个概念是有问题的,a是a,b是b,a和b并不是同一个地址。
笔者在阅读ClickHouse源码的时候发现了有趣的现象,该源码中有如下代码,我们注意第7-9行,可以发现这使用了STD的tie,该类型让C++实现了一次性返回两个值的效果。下面的executeQueryImpl函数返回了两个值,分别写入到了ast和streams中。
1 | BlockIO executeQuery( |
实际上C++中可以在结构体中指定一个引用字段,通过构造函数将外界的变量传递进结构体,再通过该结构体的拷贝构造函数实现赋值。
结构体如何储存其他值的引用?按照前文的说法,如果引用的地址是一样的,结构体如何储存其他值的引用呢,如下图。
实际上唯一的办法只能使用指针,让变量b指向变量a,当然这里的变量b的类型就是指针类型了,这么说肯定很多人不能接受,我定义的引用类型,怎么就成了指针了。
先来看下面的代码,下面这两个函数,写法不一样,但是被GCC编译器编译以后的结果是一模一样的。
1 | struct Ref { |
读者可以通过指令 gcc -S -O0 main.cpp
来编译该文件。可以看到两个函数都被编译结果为下面的内容,注意14-15行,这里就是ref赋值的地方,我们很容易发现,第一步是把rbp栈寄存器指向的地址偏移24的位置的值放入了寄存器rax中,第二步是将数据333写入rax寄存器所指向的地址。所以引用不过是指针的另一种写法而已。
1 | __Z5pointv: ## @_Z5pointv |
我们使用gcc -S -O0 main.cpp
编译下面的代码
1 | void check() { |
不难发现两个函数都被编译成了相同的代码,于是乎,现在应该不会再有人认为这里的b就是a,a就是b,a和b的地址是一样的了吧
很明显b就是指针啊,他怎么能是和a的地址相同呢?
1 | __Z5checkv: ## @_Z5checkv |
正确的引用图应该是下面这张
]]>代码中写的是数组
1 | /** |
在swagger页面展示的例子是字符串
1 | { |
Go的指针和C的指针很类似,这也是Go被归类于C类语言的原因,Go的指针不支持偏移运算,即不能向C一样让指针+1,-1。
先来看第一个,符号&
即可取到对象的地址。
1 | func sample1() { |
1 | [0 1 2 3 4 5 6 7 8 9] |
下面的输出全是9,因为for循环的value是共用一个地址的。
1 | func sample2() { |
new 只分配内存,make不仅分配内存还初始化对象。
slice、chan、map一般可以使用make初始化。
]]>在文章Go入门-Go语言从入门到进阶实战中,我们介绍了GO项目的结构,但是没有解释其中的一个文件go.mod
, 这其实是模块的意思。在go.mod中可以引入go的依赖。
1 | require ( |
这里简单介绍一下,注意到这里是库名加版本号。当我们引入了依赖管理以后,就可以在自己的项目中直接import三方包了。
Go modules 是 Go 语言的依赖解决方案,发布于 Go1.11,Go1.14 上已经明确建议生产上使用了
一开始go发布的时候是没有包管理的
go get命令会根据路径,把相应的模块获取并保存在
$GOPATH/src
也没有版本的概念,
master
就代表稳定的版本原文: 😊
在Go Module出现以前,我们使用Go Get获取库,库会直接下载到GOPATH目录的src文件夹下,很好用但是有一个问题-版本兼容问题。
当两个库依赖分别同一个库的v1和v2版本的时候,如果v1和v2不兼容,那么会导致这两个库无法同时使用。
后来官方采用了vgo方案来解决GO的依赖管理问题,也就是现在的Go modules。
go.mod控制依赖,go.sum校验依赖的完整性与正确性。
GO111MODULE=off
无模块支持,go 会从 GOPATH 和 vendor 文件夹寻找包。
GO111MODULE=on
模块支持,go 会忽略 GOPATH 和 vendor 文件夹,只根据go.mod
下载依赖。原文:😊
https://github.com/bingohuang/effective-go-zh-en
在最开始学习GO的时候,写了几篇Blog,发现代码里面的对齐都是TAB,这让我很困惑,知道现在才知道,GO语言,默认使用TAB进行对齐。
当然GO还有自己的空格规则x<<8 + y<<16
,向这份代码,我们根据空格就能知道计算的优先级了。
Go语言要求package语句前加上注释,来介绍整个包,如果package包含多个文件,则只需要在其中一个文件中标注即可。
1 | /* |
需要注意的是,注释不是JavaDoc模式的,下面这种就不对了
1 | /** |
在程序中,每个可导出(首字母大写)的名称都应该有文档注释。使用行注释的形式,行注释的第一行要以被导出的内容开头,并总结整个注释,如下,第一行以Compile开头
1 | // Compile parses a regular expression and returns, if successful, a Regexp |
Go语言默认使用驼峰命名,请不要使用下划线
包名用小写,不要带下划线,
不要使用import .
的语句,很容易冲突
Getter方法不需要Get
这个前缀,如下
1 | obj.Owner() |
只包含一个方法的接口命名以er结尾。
在Go中,行末,不要加入分号。
分号其实是由词法分析器自动增加的。
下面的这份代码,注意到其中的err申明了两次,上面这个err声明了两次,但是这并不是错误,编译器会帮忙解决这个问题,实际上err第二次只是被重新赋值而已。
1 | package main |
当然这样的技巧只能在特殊情况才能生效
在Go中,可以在函数上声明返回值变量,在返回的时候只需要一个return即可
1 | package main |
defer是一种推迟执行的语法,被该关键词修饰的语句会在函数返回的时候执行
1 | package main |
主要是init函数,一个文件可以有多个init函数,他们都会被调用,init函数在包的全局变量初始化以后执行
注意到下面这个代码,先输出abc, 与init的位置无关,init在全局变量之后执行
1 | package main |
双数组字典树英文名为DoubleArrayTrie,他的特点就是使用两个数组来表示一颗字典树,这里有比较有趣了,两个数组是怎么表达出字典树的呢?
顾名思义,有两个数组,一个是base,另一个是check。
首先介绍数组的下标,数组的下标代表字典树上节点的编号,一个下标对应一个节点。
其实base数组的作用是用来记录一个基础值,这个值可以是随机值,只要不产生冲突就可以了,所以这个值可以用随机数算法获取,当然这样效率不高,高效的做法应该是使用指针枚举技术,ok,现在你已经明白了,base数组是一个不产生冲突的随机数组。
最后,check数组,check数组与base数组相互照应,如果base[i]=check[j]
则说明j是i的儿子,而且i到j的边权恰好为j-base[i]
,也可以写作j-check[j]
好好理解这句话
从另一个方面而言,双数组字典树的base数组,应该是一个指针数组,他指向了一个长度为字符集大小的数组的首地址,而check数组是一种hash碰撞思路,由于base数组疯狂指向自己,导致产生了很多碰撞,但是由于字典树是一个稀疏图,导致儿子节点指针利用率低,所以base数组疯狂复用这段空间,最后必须要依赖check来解决冲突,
双数组字典树相比于传统字典树,仅仅只在内存方面于增删改查占有优势,但是唯一不好的地方就是删和改会导致base数组内存分裂,难以回收,删和改如果占大头,那么传统字典树的内存效率更高
由于搜索领域几乎不涉及到删和改,所以这个数据结构很nice,字符集多大,就节省了多少倍的空间
数据结构很棒,但是在现在这个内存不值钱的时代,这些指针的储存用hashmap直接无脑顶掉,空间占用也高不了多少,hashmap顶多浪费两倍空间
两倍的空间算不上啥,除非这是唯一的优化点,否则不会优化到这个数据结构上来
首先我们直接给出一颗双数组字典树,下面是三列,第一列是下标,第二列是base,第三列是check, 我们来根据这个双数组还原那颗字典树
1 | index base check |
从第一行看起,base[0]=1, check[0]=0 ,这说明根节点是0号节点。
1 | graph LR |
然后我们来找0节点的子节点,只需要哪些check值为base[0]即可,我们发现check[66],check[68],check[91]满足,所以他们都是0号节点的子节点
对于check[66], 边权为66-check[66]=65 ,是字符A
对于check[68], 边权67, 是字符C
对于check[91], 边权90,是字符Z
所以现在图成了
1 | graph LR |
接着我们看66号节点,base[66]=2,那么他的子节点为69和70,边权分别为69-check[69]=67,70-check[70]=68,分别是C和D
1 | graph LR |
69的字节点为74和75, 权重为74-5=69和75-5=70, 分别是E和F
1 | graph LR |
当然,后面的笔者就不进行模拟了,到此为止。
首先算法的第一步是构建一颗字典树,你需要将这颗字典树构建出来。字典树的代码如下,这个不用多说了
1 | void addTire(const char* str, int len) { |
第二步,就是在字典树上进行BFS,在BFS过程中增量构建双数组字典树,当我们BFS到某个节点U的时候,便开始寻找,当前还有哪个base值没有被使用,可以使得这个U的子节点的check值均为0, no BB , show code
先找到U的所有子节点
1 | vector<int> sonTransList; |
然后寻找一个合适的base值,这个值储存在变量begin中
1 | // find begin |
最后就是赋值就好了.
最后附上一个很丑的代码,第三节中介绍的双数组就来源于这个代码的输出。
1 | // ConsoleApplication1.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。 |
简单介绍Beego和Gin,水一水文章。
1 | go get github.com/astaxie/beego |
同时安装Bee工具
1 | go get github.com/beego/bee |
看到如下内容代表安装成功
1 | s@HELLOWANG-MB1 ~ % go get github.com/astaxie/beego |
注意安装以后对二进制文件在$GOPATH/bin下,所以请确保该路径在你的环境变量下,笔者使用MACOS,在~/.zshrc
中使用如下配置
1 | GOPATH=~/go |
1 | s@HELLOWANG-MB1 src % bee new bee-demo |
1 | s@HELLOWANG-MB1 bee-demo % go get |
在GoLand IDE中点击▶启动项目,看到如下输出,访问http://localhost:8080/即可看到小蜜蜂
1 | GOROOT=/usr/local/Cellar/go/1.17.2/libexec #gosetup |
路由在文件routers/router.go
中, 在Beego的设计中,路由需要手动添加,即哪个PATH交给哪个Controller来处理。
1 | package routers |
MainController继承了beego.Controller,所以我们可以复写他的方法,注意到beego.Controller实现了beegoControllerInterface这个接口,里边的方法其实都比较明显了,就是HTTP协议的方法。
1 | type ControllerInterface interface { |
首先编写一个HelloController,然后把它加入到route中,最后访问/hello,就可以看到输出了。
1 | package controllers |
1 | GET /hello |
这一块就点到为止了,听说Beego框架不太火,了解了解就好了,笔者也不是文档翻译器,更多更加细节的部分关注这里
1 | go get github.com/gin-gonic/gin |
编写下面的代码,然后直接运行,访问http://localhost:8080/即可看到Hello World
1 | package main |
日志如下
1 | GOROOT=/usr/local/Cellar/go/1.17.2/libexec #gosetup |
更加具体的,参见文档,太没技术的东西笔者不喜欢写。
]]>很多语言都支持协程,那什么是协程,和线程进程有什么区别呢?这里推荐一篇Blog,笔者直接提取其中最重要的部分
进程、线程 和 协程 之间概念的区别
对于 进程、线程,都是有内核进行调度,有 CPU 时间片的概念,进行 抢占式调度(有多种调度算法)
对于 协程(用户级线程),这是对内核透明的,也就是系统并不知道有协程的存在,是完全由用户自己的程序进行调度的,因为是由用户程序自己控制,那么就很难像抢占式调度那样做到强制的 CPU 控制权切换到其他进程/线程,通常只能进行 协作式调度,需要协程自己主动把控制权转让出去之后,其他协程才能被执行到。
goroutine 和协程区别
本质上,goroutine 就是协程。 不同的是,Golang 在 runtime、系统调用等多方面对 goroutine 调度进行了封装和处理,当遇到长时间执行或者进行系统调用时,会主动把当前 goroutine 的CPU (P) 转让出去,让其他 goroutine 能被调度并执行,也就是 Golang 从语言层面支持了协程。Golang 的一大特色就是从语言层面原生支持协程,在函数或者方法前面加 go关键字就可创建一个协程。
操作系统是不知道协程的,那么应用层如何实现协程呢?下面给一些伪代码
1 | 不断循环: |
我们可以看到,其实这里正在执行的任务就是协程,这样的线程模型,他的CPU利用率非常高,他的协程切换代价非常低,几乎只需要入队出队而已。
但是这样的模型有一个很大的缺点,那就是CPU的公平性,如果一个协程迟迟不退出,且不进行系统调用,也不主动释放CPU,那么,这个协程将造成队头阻塞现象。
想必很多人都知道这个名词,这其实就是Go的协程(coroutine), 相比于普通的协程,Go做了特殊的处理,能够在一些适当的时候,交出CPU控制权,所以Go的调度是公平的。
当然Go关于协程,也有一个Bug,这个Bug在Go1.14的时候被修复了,有兴趣的读者可以自己查看,当然笔者在下文也会通过一个更加简单的例子来复现这个Bug。
Bug的复现需要两个版本的Go进行对比才能看出来,笔者建议使用Docker。首先就是给出代码了,下面的代码启用了两个协程,第一个做CPU空轮询,第二个做不间断输出。
1 | package main |
用docker启动容器,用cat写入代码
1 | docker run -it --rm golang:1.13 bash |
然后运行
1 | go run a.go |
读者可以很容易地发现,这份代码只会输出一个begin1
这次,进入docker的指令换成下面这条就行了
1 | docker run -it --rm golang:1.14 bash |
在1.14中,会输出如下内容
1 | begin2 |
由此可见,Go1.14解决了for循环长时间占用CPU的问题。
要注意释放的时机,函数调用、for循环中都可以,但是要注意控制释放的频率,不能太频繁,也不能太不频繁。
]]>从一本书开始,这本书叫做《Go语言从入门到进阶实战(视频教学版)》,当然这篇Blog并不是所有的内容都来自这本书,毕竟书中也有不足之处。
学啥语言的第一步都是Hello World, 第一步是搭建开发环境,直接下载Goland,创建一个新的工程,点击create
建好项目以后,项目应该是下面这个样子的,当然我们不用关注这个go.mod是干什么的,我们只是利用IDE编写代码,使用命令行运行。
1 | s@HELLOWANG-MB1 go-study % tree |
然后新建一个main.go的文件,其内容如下
1 | package main |
最后运行该文件
1 | s@HELLOWANG-MB1 go-study % go run main.go |
我们也可以编译为二进制文件,然后运行
1 | s@HELLOWANG-MB1 go-study % go build main.go |
变量的声明方式为var <变量名> <变量类型>
例如下面的程序
1 | package main |
经过了命令行运行以后,可以转而使用IDE集成开发,点击按钮▶️
当然,还有更多的声明方式
1 | package main |
最后还有一个小技巧,即函数返回可以是多个变量,用逗号分隔, _
表示匿名变量,即忽略该位置的值。
1 | package main |
有符号整型: int8 int16 int32 int64
无符号整型: uint8 uint16 uint32 uint64
平台自适应整形: int uint
(自动根据平台决定整形的长度)
float32 float64
1 | package main |
1 | package main |
1 | package main |
多行字符串
1 | package main |
1 | package main |
1 | package main |
需要注意的是Go的switch自带break,下面的程序只会输出3
1 | package main |
注意map需要指定key和value 的类型
1 | package main |
数组长度固定, 创建数组也有很多种写法
1 | package main |
注意对于由字面量组成的数组,如果长度小于等于4,那么它将直接被分配到栈上,否则分配到静态区
切片的创建可以是数组的一部分,也可以直接创建,切片可扩容
1 | package main |
那么切片和数组有什么区别呢?其实切片只是数组的一个引用,任何一个切片,其背后一定有一个数组,当切片进行扩容的时候,会根据数组的剩余空间大小来决定附身到新的数组上,或者直接在原数组上扩容切片。
具体表现如下, 输出就在注释里面。
1 | package main |
列表可以自动伸缩
1 | package main |
返回值写在参数后面
1 | package main |
还可以选择返回多个值
1 | package main |
变长的参数
1 | package main |
返回一个函数
1 | package main |
和C一样
1 | type <类型名> struct { |
例子:
1 | type Point struct { |
1 | package main |
使用new实例化,注意此时得到的p2是指针
1 | package main |
结构体函数定义比普通函数定义在func和函数名之间多了一个结构体对象,这个对象一般使用指针
1 | package main |
结构体中的字段可以设置Tag,即给字段打上标签,就像Java中的注解一样。然后可以使用一种比较高级的技术(反射来获取这个标签)
1 | package main |
接口定义
1 | type <接口类型名> interface{ |
让结构体实现接口, 只需要让结构体的函数与接口保持一致即可,
1 | package main |
回到最开始的Helloworld,注意到第一行中的package main
, 在Go中,有这样一个约定
包名为main的包为应用程序的入口包,编译源码没有main包时,将无法编译输出可执行的文件。
1 | package main |
在Go中,首字母为小写的变量只能在包内使用,首字母为大写的变量会自动导出,可以在其他包使用。
这是第一个文件mylib/mylib.go
1 | package mylib |
然后是main.go
, 注意到可以直接使用Add
函数,但是不能使用add
函数
1 | package main |
在导入的时候可以直接重命名,只需要在包名前加上一个名字即可
1 | package main |
一个包的init函数在包被引入时自动调用, 对于main包,init函数在main函数前运行
1 | package mylib |
1 | package main |
通过reflect包来进行反射,可以获得类型
1 | package main |
对于name和kind的区别,看看下面这份代码就行了, name为Point,kind为struct
1 | package main |
通过字段的名字获取属性
1 | package main |
在关键词go后跟着一个函数调用,那么该函数调用就变成了goroutine,这是一个异步调用,立即返回
1 | package main |
下面的程序会依次输出0和hello
, 通道先进先出 <-
符号可以用来传输数据, 注意通道在发送和接受的时候都会阻塞,注意到最后一行有一个time.Sleep(time.Second)
,这是为了等待goroutine完成,否则main退出以后goroutine会直接强制退出
1 | package main |
另一种接受方法是使用for循环,代码如下,注意这个循环需要手动退出
1 | package main |
最后通道还支持指定输入端和输出端,输入端只能做输入,输出端只能做输出
1 | package main |
创建带有缓冲区的通道, 只需要在make的第二个参数中填入数字即可
1 | ch := make(chan interface{}, 10) |
多路复用,使用select关键字,case区域写要选择的通道即可接收多个通道,下面的代码有时输出1,有时输出2
1 | package main |
给磁盘分区
1 | fdisk /dev/vdc |
格式化分区
1 | mkfs.ext4 /dev/vdc |
挂载磁盘
1 | mkdir -p /data/ssd && mount /dev/vdc /data/ssd |
1 | systemctl list-unit-files |
会看到有两列,左侧是服务的名字,右侧是服务的状态,enabled代表开机自启
1 | UNIT FILE STATE |
1 | systemctl disable kubelet |
1 | systemctl enable kubelet |
jps 可以看到运行中的java进程
1 | sh-4.2$ jps |
jcmd可以看到运行中的java进程以及参数
1 | sh-4.2$ jcmd |
jmap是可以查看整个JVM内存的工具。
jmap -heap <pid>
即可查看JVM堆堆使用情况,主要有两块,一块是Heap配置,另一块是Heap使用情况。
Heap配置中包含了最小堆空余空间比例、最大比例、最大堆大小、新生代大小、老生代大小、新生代比例、老生代比例、原空间大小等等。这部分是Java进程启动的时候由JVM参数决定的
Heap Usage是堆的使用情况,包括新生代使用情况、老生代使用情况(容量、使用、未使用)
1 | sh-4.2$ jmap -heap 364 |
jmap -histo <pid>
可以查看JVM内存中每个类的内存使用情况
1 | sh-4.2$ jmap -histo 370 | less |
jmap -finalizerinfo <pid>
可以看到有多少个对象在等待finalizer
1 | sh-4.2$ jmap -finalizerinfo 370 |
jmap -dump:format=b,file=dump.bin 370
可以把堆中的数据导出到二进制文件中
1 | sh-4.2$ jmap -dump:format=b,file=dump.bin 370 |
jstack <pid>
可以看到所有线程目前的栈信息。
1 | "Finalizer" #3 daemon prio=8 os_prio=0 tid=0x00007fd8506eb000 nid=0x1a7 in Object.wait() [0x00007fd77b826000] |
查看options
1 | sh-4.2$ jstat -options |
查看类的个数, 每1000毫秒打印一次,一共打印10次
1 | sh-4.2$ jstat -class 370 1000 10 |
查看JIT实时编译器编译的情况
1 | sh-4.2$ jstat -compiler 370 1000 3 |
查看GC情况,每一列分别对应
S0C | 第一个survivor的容量 | |
S1C | 第二个survivor的容量 | |
S0U | 第一个survivor的使用空间 | |
S1U | 第二个survivor的使用空间 | |
EC | eden容量 | eden和survivor的比例是8:1:1 |
EU | eden使用空间 | |
OC | 老年代容量 | |
OU | 老年代使用空间 | |
MC | 方法区的容量 | |
MU | 方法区使用空间 | |
CCSC | 压缩类容量 | 注意被压缩的不是类,而是对象头中指向类的指针被压缩成32位了 |
CCSU | 压缩类使用空间 | |
YGC | YGC的次数 | |
YGCT | YGC的时间 | |
FGC | FULL GC的次数 | |
FGCT | FULL GC的时间 | |
GCT | GC总时间 |
1 | sh-4.2$ jstat -gc 370 1000 3 |
主要关注各区域最大最小空间
1 | sh-4.2$ jstat -gccapacity 370 1000 3 |
主要关注导致GC的原因
1 | sh-4.2$ jstat -gccause 370 1000 3 |
主要关注内存占比
1 | sh-4.2$ jstat -gcutil 370 1000 3 |
JIT的方法
1 | sh-4.2$ jstat -printcompilation 370 1000 3 |
jinfo能输出一个进程的基本信息,包括他的配置、用户、路径、命令行等等
1 | sh-4.2$ jinfo 370 |
jhat是堆快照分析工具,用于分析堆的情况,首先要用jmap导出堆到文件dump.bin中,然后使用jhat分析,再访问7000端口即可
1 | sh-4.2$ jhat dump.bin |
用于编写JNI方法
字节码分析工具
Java交互式工具
1 | sh-4.2$ jshell |