-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.xml
More file actions
507 lines (457 loc) · 68.4 KB
/
index.xml
File metadata and controls
507 lines (457 loc) · 68.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>Gopher指北</title>
<link>https://isites.github.io/</link>
<description>Recent content on Gopher指北</description>
<generator>Hugo -- gohugo.io</generator>
<language>en-us</language>
<lastBuildDate>Mon, 11 Apr 2022 08:38:48 +0800</lastBuildDate><atom:link href="https://isites.github.io/index.xml" rel="self" type="application/rss+xml" />
<item>
<title>现在,这个酷炫的github首页是你们的了!</title>
<link>https://isites.github.io/timeline/github-profile/</link>
<pubDate>Mon, 11 Apr 2022 08:38:48 +0800</pubDate>
<guid>https://isites.github.io/timeline/github-profile/</guid>
<description>装逼方法千千万,guthub占一半。几乎每一位求职者都乐意在简历中贴上自己的github地址。事情的起源也是因为我最近查看候选人简历上的github时,发现其首页酷炫无比。既然被我发现,那现在我也拥有一个酷炫的首页了!
正所谓“吃水不忘挖井人”,用了别人的东西不提一嘴是不合适的。这个酷炫的github首页,我基本没做什么额外的工作,完全依靠各位巨人,尤其要感谢下面两篇文章。
https://blog.holic-x.com/wv-blog/post/7ad96a5d.html
https://github.com/anuraghazra/github-readme-stats/blob/master/docs/readme_cn.md
接下来我们分步骤开始构建一个酷炫的github首页。首先,创建一个和github用户名同名的仓库。以老许的为例,创建同名仓库时会有如下提示,看到这个提示也就证明我们从0走到了1。
仓库创建好了之后,我们只需要编辑README.md,其内容将会展示在github首页。常规的文字编辑老许就不多bb了,我们直奔主题想办法让其酷炫起来。
GitHub统计卡片 前人栽树后人乘凉,最常规最简单的统计卡片生成只需要使用如下代码即可(将${username}替换为自己的github用户名)。
 卡片还有很多别的参数可以定义,老许就不一一阐述,这里直接贴上我的自定义参数count_private=true&amp;show_icons=true&amp;theme=github。其展示效果如下。
热门语言卡片 热门语言卡片使用方式同样简单,只需要使用如下代码即可(将${username}替换为自己的github用户名)。
 同样,老许也根据自己情况增加了自定义参数hide=Java,HTML,PHP&amp;layout=compact&amp;theme=github,其展示效果如下。
GitHub活动统计图 GitHub活动统计图使用如下代码即可(将${username}替换为自己的github用户名)。
 为了能够和GitHub统计卡片以及热门语言卡片表现更加统一,老许增加了自定义参数theme=github-light,其展示效果如下。
最终,老许的github首页展示效果如下。
为了能够让各位读者把我的首页变成你们自己的首页,欢迎访问老许的github(https://github.com/Isites)借鉴。
【关注公众号】</description>
</item>
<item>
<title>泛型来了,那什么时候适用泛型,什么时候适用接口?</title>
<link>https://isites.github.io/timeline/go-generics/</link>
<pubDate>Mon, 21 Mar 2022 08:48:00 +0800</pubDate>
<guid>https://isites.github.io/timeline/go-generics/</guid>
<description>什么是泛型 借助泛型,你可以先编写数据结构和函数,稍后再指定其中的类型。在当前的Go中,函数当然有参数,使用泛型后,函数可以拥有一种新的参数,称为“类型参数”。而且以前不能拥有任何参数的类型本身,也可以有类型参数。使用类型参数的函数和类型,可以使用类型实参来实例化。
对于类型参数,我们会说“实例化”而不是调用。这是因为相关操作完全在编译阶段而不是在运行时发生。类型参数具有限制条件,限制允许的类型实参集,就像普通参数具有类型限制允许的普通实参集一样。例如,下面的函数接受一个map[string]int类型参数,并返回该类型中所有键的切片。
func MapKeys(m map[string]int) []string{ var s []string for k := range m { s = append(s, k) } } return s 你可以轻松地为任何特定的map类型编写这个函数,但是你需要为要使用的每种映射类型编写一个不同的函数副本。或者,也可以使用reflect包编写此函数,但编写起来很费劲且函数运行速度相对较慢。使用reflect包的过程非常复杂,我就不举例说明了。现在,你也可以使用类型参数,借助类型参数你只需要编写一次这个函数,就可以适用于所有的映射类型。同时,编译器会对其进行全面的类型检查。
func MapKeys[K comparable, V any](m map[K]V) []K { var s []K for k := range m { s = append(s, k) } return s } 上述代码中,类型参数名为K和V,普通参数m以前的类型为map[string]int,现在的类型为map[K]V。类型参数K是映射的键类型因此必须可以进行比较,这通过为K预先声明comparable限制条件来明确表达,你可以将其视为该类型参数的元类型。类型参数V可以是任意类型,因此它的限制是预先声明的限制条件any。函数主体和以前一样,只是变量s现在是k的类型切片而不是字符串的类型切片。关于泛型还有很多其他细节,在这里就不继续讨论了,有兴趣的可以去阅读官方的使用手册。非常重要的一点是,虽然这个示例中没有展示,但实际上类型本身也可以有类型参数。
什么时候适用泛型 言归正传,今天并不是要讨论什么是泛型或者如何使用他们,而是讨论在什么情况下应该适用泛型以及什么情况下不适用。需要明确的是这个讲座只提供一般指导并不是硬性规定,具体情况由你自行判断。但是,如果你不确定,可以参考我将要讨论的准则。
首先,我来谈谈使用Go编程的一般准则。我们是通过编写代码来编写Go程序而不是通过定义类型。
Write code, don&#39;t design types 当涉及到泛型时,如果你通过定义类型参数限制条件来开始编写程序则可能走错了方向。首先,应编写函数,然后当你清楚地看到可以使用类型参数时再轻松地添加。
为了说明这一点,现在我们看看类型参数在什么情况下可能有用。</description>
</item>
<item>
<title>【重要】这个布谷鸟的库有一个坑,请尽快更新!</title>
<link>https://isites.github.io/timeline/cuckoo-pr/</link>
<pubDate>Mon, 14 Mar 2022 08:38:00 +0800</pubDate>
<guid>https://isites.github.io/timeline/cuckoo-pr/</guid>
<description>有没有,存不存在?这是一个很经典的场景,几乎每一位互联网开发者都遇到过。其对应的解决方案也比较多。布谷鸟过滤器就是其中一种十分流行的方案。
当然,本篇并不是来介绍布谷鸟过滤器的原理,而是记录一个老许在使用一个布谷鸟过滤器开源库时遇到的坑。如果有读者和老许使用了相同的开源库,请及时更新最新的代码以避免线上panic。当然,如果是自实现的布谷鸟过滤器,老许愿称你为:
其他废话不多说,我们来看一下这个开源库和坑分别是什么。
开源库:
https://github.com/seiflotfy/cuckoofilter
坑:
func TestService_getInstalledApps(t *testing.T) { // 从缓存或者其他地方取出布谷鸟过滤器的数据,解析得到布谷鸟过滤器实例 c, err := cuckoo.Decode([]byte(&#34;&#34;)) assert.Nil(t, err) // 查询 test 是否存在 assert.False(t, c.Lookup([]byte(&#34;test&#34;))) } 上面这个单元测试是无法正常运行的,其结果如下:
--- FAIL: TestService_getInstalledApps (0.00s) panic: runtime error: index out of range [3532051776] with length 0 [recovered] panic: runtime error: index out of range [3532051776] with length 0 goroutine 19 [running]: testing.tRunner.func1.2({0x102e36060, 0x140000e4240}) /usr/local/go/src/testing/testing.go:1209 +0x258 testing.tRunner.func1(0x140000fe680) /usr/local/go/src/testing/testing.go:1212 +0x284 panic({0x102e36060, 0x140000e4240}) /usr/local/go/src/runtime/panic.</description>
</item>
<item>
<title>除以零不会panic?</title>
<link>https://isites.github.io/timeline/divide0/</link>
<pubDate>Tue, 08 Mar 2022 08:48:00 +0800</pubDate>
<guid>https://isites.github.io/timeline/divide0/</guid>
<description>有常识的人都知道,在除法运算中不能除以零,而我们在实际的应用中面对大量的上下文,很有可能因为考虑不周就出现除以零的情况。因此,我们有必要知道除以零到底会不会panic?如果不发生panic,又会得到什么样的值?
下面,我们带着这样的疑问继续阅读本文。相信在读完本文后,这两个疑问会烟消云散。同时,为了能够让读者快速地了解本文的全貌,下面列出本文的大纲。
除以零值 在Go中,可能除以零的情况分为三种,分别是除以常量0、整形0和浮点数0。下面我们分别看一下这三种情况的实际表现。
除以常量0 根据上图知道,除以常量0是无法编译通过的。这一点,还是比较令人安心。
除以整形0 根据上图知,除以整形0会发生panic。这一点,在平时的开发中还需格外注意。
除以浮点数0 除以浮点数0,情况会略微复杂。请看下代码和输出结果。
var zero = float64(0) fmt.Println(1024 / zero) // 输出:+Inf fmt.Println(-1024 / zero) // 输出:-Inf fmt.Println(0 / zero) // NaN 上面输出中Inf为单词infinity的缩写,该单词含义为无穷,因此+Inf和-Inf分别表示正无穷和负无穷。
NaN意味着not a number,即结果不是一个数。
到这里,老许不得不感叹浮点数确实博大精深,在Go里面除以0确实不会panic(经过老许验证,在python里面会发生错误)。另外,上述中0/zero得到NaN,而整形中0除以0依旧会panic。
±Inf值 判断是否是±Inf 前面通过正数和负数分别除以浮点数0可到正无穷和负无穷。Go里面math包提供的Inf函数也可以得到正无穷和负无穷,同时还提供了IsInf函数用于判断是正无穷还是负无穷。
math.Inf函数签名为func(int) float64,当传入的参数大于等于0时返回正无穷,否则返回负无穷。
math.IsInf函数签名为func(float64, int) bool,第一个参数为待判断的值,第二个参数大于0时,返回第一个参数是否为正无穷,第二个参数小于0时,返回第一个参数是否为负无穷,第二个参数等于0时,返回第一个参数是否为无穷。
具体验证,请看下面代码和输出。
positiveInf := math.Inf(1) negativeInf := math.Inf(-1) // 判断是否为正无穷 fmt.Println(math.IsInf(positiveInf, 1)) // 输出:true // 判断是否为负无穷 fmt.Println(math.IsInf(negativeInf, 1)) // 输出:false // 判断是否为正无穷 fmt.Println(math.IsInf(positiveInf, -1)) // 输出:false // 判断是否为负无穷 fmt.Println(math.IsInf(negativeInf, -1)) // 输出:true // 判断是否为无穷 fmt.</description>
</item>
<item>
<title>用Go构建你专属的JA3指纹</title>
<link>https://isites.github.io/timeline/go-ja3/</link>
<pubDate>Mon, 28 Feb 2022 08:00:00 +0800</pubDate>
<guid>https://isites.github.io/timeline/go-ja3/</guid>
<description>在这篇文章中将会简单回顾https的握手流程,并基于读者的提问题解释什么是JA3指纹以及如何用Go定制专属的JA3指纹。
本文大纲如下,请各位读者跟着老许的思路逐步构建自己专属的JA3指纹。
回顾HTTPS握手流程 在正式开始了解什么是JA3指纹之前,我们先回顾一下HTTPS的握手流程,这将有助于对后文的理解。
在码了2000多行代码就是为了讲清楚TLS握手流程这篇文章中主要分析了HTTPS单向认证和双向认证流程(TLS1.3)。
在单向认证中,客户端不需要证书,只需验证服务端证书合法即可。其握手流程和交换的msg如下。
在双向认证中,服务端和客户端均需验证对方证书的合法性。其握手流程和交换的msg如下。
单向认证和双向认证的对比:
单向认证和双向认证中,总的数据收发仅三次,单次发送的数据中包含一个或者多个消息
clientHelloMsg和serverHelloMsg未经过加密,之后发送的消息均做了加密处理
Client和Server会各自计算两次密钥,计算时机分别是读取到对方的HelloMsg和finishedMsg之后
双向认证和单向认证相比,服务端多发送了certificateRequestMsgTLS13消息
双向认证和单向认证相比,客户端多发送了certificateMsgTLS13和certificateVerifyMsg两个消息
无论是单向认证还是双向认证,Server对于Client的基本信息了解完全依赖于Client主动告知Server,而其中比较关键的信息分别是客户端支持的TLS版本、客户端支持的加密套件(cipherSuites)、客户端支持的签名算法和客户端支持的密钥交换协议以及其对应的公钥。这些信息均在包含clientHelloMsg中,而这些信息也是生成JA3指纹的关键信息,并且clientHelloMsg和serverHelloMsg未经过加密。未加密意味着修改难度降低,这也就为我们定制自己专属的JA3指纹提供了可能。
如果有兴趣了解HTTPS握手流程的更多细节,请阅读下面文章:
码了2000多行代码就是为了讲清楚TLS握手流程
码了2000多行代码就是为了讲清楚TLS握手流程(续)
什么是JA3指纹 前面说了这么多,那么到底什么是JA3指纹呢。根据Open Sourcing JA3这篇文章,老许简单将其理解为JA3就是一种在线识别TLS客户端指纹的方法。
该方法用于收集clientHelloMsg数据包中以下字段的十进制字节值:TLS Version、Accepted Ciphers、List of Extensions、Elliptic Curves和Elliptic Curve Formats。然后,它将这些值串联起来,使用“,”来分隔各个字段,同时使用“-”来分隔各个字段中的值。最后,计算这些字符串的md5哈希值,即得到易于使用和共享的长度为32字符的指纹。
为了更近一步描述清楚这些数据的来源,老许将John Althouse文章中的抓包图结合Go源码中的clientHelloMsg结构体做了字段一一映射。
细心的同学可能已经发现了,根据前文描述JA3指纹总共有5个数据字段,而上图却只映射了4个。那是因为TLS的extension字段比较多,老许就不一一整理了。虽然没有一一列举,但老许准备了一个单元测试,有兴趣深入研究的同学可以通过这个单元测试进行调试分析。
https://github.com/Isites/go-coder/blob/master/http2/tls/handsh/msg_test.go JA3指纹用途 根据前文的描述,JA3指纹就是一个md5字符串。请大家回想一下在平时的开发中md5的用途。
判断内容是否一致 作为唯一标识 md5虽然不安全,但是JA3选择md5作为哈希的主要原因是为了更好的向后兼容
很明显,JA3指纹也有其类似用途。举个简单的例子,攻击者构建了一个可执行文件,那么该文件的JA3指纹很有可能是唯一的。因此,我们能通过JA3指纹识别出一些恶意软件。
在本小节的最后,老许给大家推荐一个网站,该网站挂出了很多恶意JA3指纹列表。
https://sslbl.abuse.ch/ja3-fingerprints/ 构建专属的JA3指纹 http1.1的专属指纹 前文提到clientHelloMsg和serverHelloMsg未经过加密,这为定制自己专属的JA3指纹提供了可能,而在github上面有一个库(https://github.com/refraction-networking/utls)可以在一定程度上修改clientHelloMsg。下面我们将通过这个库构建一个自己专属的JA3指纹。
// 关键import import ( xtls &#34;github.com/refraction-networking/utls&#34; &#34;crypto/tls&#34; ) // 克隆一个Transport tr := http.</description>
</item>
<item>
<title>一顿骚操作版本号比较性能提升300%</title>
<link>https://isites.github.io/timeline/version-compare/</link>
<pubDate>Mon, 24 Jan 2022 20:30:38 +0800</pubDate>
<guid>https://isites.github.io/timeline/version-compare/</guid>
<description>在一次性能分析中,发现线上服务CompareVersion占用了较长的CPU时间。如下图所示。
其中占用时间最长的为strings.Split函数,这个函数对Gopher来说应该是非常熟悉的。而CompareVersion就是基于strings.Split函数来实现版本比较的,下面看一下CompareVersion的实现。
// 判断是否全为0 func zeroRune(s []rune) bool { for _, r := range s { if r != &#39;0&#39; &amp;&amp; r != &#39;.&#39; { return false } } return true } // CompareVersion 比较两个appversion的大小 // return 0 means ver1 == ver2 // return 1 means ver1 &gt; ver2 // return -1 means ver1 &lt; ver2 func CompareVersion(ver1, ver2 string) int { // fast path if ver1 == ver2 { return 0 } // slow path vers1 := strings.</description>
</item>
<item>
<title>探讨系统中💰钱的精度问题</title>
<link>https://isites.github.io/timeline/money/</link>
<pubDate>Mon, 20 Dec 2021 12:20:38 +0800</pubDate>
<guid>https://isites.github.io/timeline/money/</guid>
<description>钱,乃亘古之玄物,有则气粗神壮,缺则心卑力浅
在一个系统中,特别是一个和钱相关的系统,钱乃重中之重,计算时的精度将是本篇讨论的主题。
精度为何如此重要 “积羽沉舟”用在此处最为合适。假如某电商平台每年订单成交数量为10亿,每笔订单少结算1分钱,则累计损失1000万!有一说一,这损失的钱就是王某人的十分之一个小目标。如果因为精度问题在给客户结算时,少算会损失客户,多算会损失钱。由此可见,精确的计算钱十分重要!
为什么会有精度的问题 经典案例,我们来看一下0.1 + 0.2在计算机中是否等于0.3。
上述case学过计算机的应该都知道,计算机是二进制的,用二进制表示浮点数时(IEEE754标准),只有少量的数可以用这种方法精确的表示出来。下面以0.3为例看一下十进制转二进制小数的过程。
计算机的位数有限制,因此计算机用浮点数计算时肯定无法得到精确的结果。这种硬限制无法突破,所以需要引入精度以保证对钱的计算在允许的误差范围内尽可能准确。
关于浮点数在计算机中的实际表示本文不做进一步讨论,可以参考下述连接学习:
单精度浮点数表示:
https://en.wikipedia.org/wiki/Single-precision_floating-point_format
双精度浮点数表示:
https://en.wikipedia.org/wiki/Double-precision_floating-point_format
浮点数转换器:
https://www.h-schmidt.net/FloatConverter/IEEE754.html
用浮点数计算 还是以上述0.1 + 0.2为例,0.00000000000000004的误差完全可以忽略,我们尝试小数部分保留5位精度,看下面结果。
此时的结果符合预期。这也是为什么很多时候判断两个浮点数是否相等往往采用a - b &lt;= 0.00001的形式,说白了这就是小数部分保留5位精度的另一种表现形式。
用整型计算 前面提到只有少量的浮点数可以用IEEE754标准表示,而整型可精确表示所有有效范围内的数。因此很容易想到,使用整型表示浮点数。
例如,事先定好小数保留8位精度,则0.1和0.2分别表示成整数为10000000和20000000, 浮点数的运算也就转换为整型的运算。还是以0.1 + 0.2为例。
// 表示小数位保留8位精度 const prec = 100000000 func float2Int(f float64) int64 { return int64(f * prec) } func int2float(i int64) float64 { return float64(i) / prec } func main() { var a, b float64 = 0.</description>
</item>
<item>
<title>终于解决了这个线上偶现的panic问题</title>
<link>https://isites.github.io/timeline/defer-sync-pool/</link>
<pubDate>Tue, 23 Nov 2021 12:20:38 +0800</pubDate>
<guid>https://isites.github.io/timeline/defer-sync-pool/</guid>
<description>不知道其他人是不是这样,反正老许最怕听到的词就是“偶现”,至于原因我不多说,懂的都懂。
下面直接看panic信息。
runtime error: invalid memory address or nil pointer dereference panic(0xbd1c80, 0x1271710) /root/.go/src/runtime/panic.go:969 +0x175 github.com/json-iterator/go.(*Stream).WriteStringWithHTMLEscaped(0xc00b0c6000, 0x0, 0x24) /go/pkg/mod/github.com/json-iterator/go@v1.1.11/stream_str.go:227 +0x7b github.com/json-iterator/go.(*htmlEscapedStringEncoder).Encode(0x12b9250, 0xc0096c4c00, 0xc00b0c6000) /go/pkg/mod/github.com/json-iterator/go@v1.1.11/config.go:263 +0x45 github.com/json-iterator/go.(*structFieldEncoder).Encode(0xc002e9c8d0, 0xc0096c4c00, 0xc00b0c6000) /go/pkg/mod/github.com/json-iterator/go@v1.1.11/reflect_struct_encoder.go:110 +0x78 github.com/json-iterator/go.(*structEncoder).Encode(0xc002e9c9c0, 0xc0096c4c00, 0xc00b0c6000) /go/pkg/mod/github.com/json-iterator/go@v1.1.11/reflect_struct_encoder.go:158 +0x3f4 github.com/json-iterator/go.(*structFieldEncoder).Encode(0xc002eac990, 0xc0096c4c00, 0xc00b0c6000) /go/pkg/mod/github.com/json-iterator/go@v1.1.11/reflect_struct_encoder.go:110 +0x78 github.com/json-iterator/go.(*structEncoder).Encode(0xc002eacba0, 0xc0096c4c00, 0xc00b0c6000) /go/pkg/mod/github.com/json-iterator/go@v1.1.11/reflect_struct_encoder.go:158 +0x3f4 github.com/json-iterator/go.(*OptionalEncoder).Encode(0xc002e9f570, 0xc006b18b38, 0xc00b0c6000) /go/pkg/mod/github.com/json-iterator/go@v1.1.11/reflect_optional.go:70 +0xf4 github.com/json-iterator/go.(*onePtrEncoder).Encode(0xc002e9f580, 0xc0096c4c00, 0xc00b0c6000) /go/pkg/mod/github.com/json-iterator/go@v1.1.11/reflect.go:219 +0x68 github.com/json-iterator/go.(*Stream).WriteVal(0xc00b0c6000, 0xb78d60, 0xc0096c4c00) /go/pkg/mod/github.com/json-iterator/go@v1.1.11/reflect.go:98 +0x150 github.com/json-iterator/go.(*frozenConfig).Marshal(0xc00012c640, 0xb78d60, 0xc0096c4c00, 0x0, 0x0, 0x0, 0x0, 0x0) 首先我坚信一条,开源的力量值得信赖。因此老许第一波操作就是,分析业务代码是否有逻辑漏洞。很明显,同事也是值得信赖的,因此果断猜测是某些未曾设想到的数据触发了边界条件。接下来就是保存现场的常规操作。
如标题所说,这是偶现的panic问题,因此按照上面的分类采用符合当前技术栈的方法保存现场即可。接下来就是坐等收获的季节,而这一等就是好多天。中间数次收到告警,却没有符合预期的现场。</description>
</item>
<item>
<title>浅谈在线广告分配策略</title>
<link>https://isites.github.io/timeline/ad-msvv/</link>
<pubDate>Sat, 30 Oct 2021 12:20:38 +0800</pubDate>
<guid>https://isites.github.io/timeline/ad-msvv/</guid>
<description>在线广告,也称网络广告、互联网广告,顾名思义,指的是在线媒体上投放的广告。平时我们在刷信息流、短视频、新闻和微博均可以看见它的影子。对于比较大的广告平台,用户定向后依旧会有大量的广告可以下发,而从大量的广告中选择合适的广告展现给用户就是本篇要讨论的主题——在线广告分配策略。
名词描述 为了更好的理解本文,先提前做一些名词描述。
eCPM(Effective Cost Per Mille): 指的是每一千次展示可以获得的广告收入。此指标反映盈利能力,不代表实际收入。不同的广告主会选择CPC、CPM等不同出价方式,因此广告分配时无法以纯粹的出价进行比较,所以才有了ecpm这一指标用于评估不同出价方式的广告可以给广告平台带来的收益。
定向广告:所谓&quot;定向&quot;实际上是对受众的筛选,即广告的显示是根据访问者来决定的,先进的广告管理系统 能够提供多种多样的定向方式。
最好的一定合适嘛 对于广告平台而言收益最大化是优先事项。为了保证收益最大化,对于每一次广告请求我们都选择ecpm最高的广告下发。这个逻辑从理论上来看是正确的,但在实际中就不一定了,那么它到底会有什么问题呢?
⼴告消耗超预算限额。 广告预算消耗不尽。 空结果问题。 部分广告消耗过快影响广告主投放体验和用户产品体验。 问题分析 问题1
对问题1进行分析时,我们需要先有这样一个共识,广告的点击、曝光等数据上报有一定的延时。
由于广告分配策略未考虑预算消耗信息,当消耗接近预算限额时未能及时减缓曝光速度,导致本应分配给其他广告主的流量依旧分配给了预算受限的广告主,这是对广告平台流量的浪费(流量越大的平台浪费会愈加严重)。
问题2
部分中小广告主竞争力弱(出价低),很难获取足够的曝光量,这种情形当广告充裕时尤为明显。
问题3
一方面可能是因为广告资源不足,另外一方面也有可能是定向广告消耗过快(详见下面的例子)。
问题4
广告按照ecpm排序,会导致广告消耗速度差异较大直接影响广告主的投放体验,甚至于用户反复看到重复的广告直接影响用户产品体验,再反过来影响到广告的CVR。
为了进一步说明纯按价高者得这一算法的不足之处,请看下面的特殊例子。
广告 出价($) 预算($) 定向 A 0.5 100 男,游戏 B 1 100 男,游戏,运动 以上述广告为例,现有男,游戏和男,运动请求各100。理想最大收益为150$,但是按照上述策略分配广告时,会出现男,游戏这100请求先到达时优先消耗B广告,男,运动这100请求达到时无广告可消耗。按照ecpm排序的算法又称为Greedy算法,该算法会让高价值广告快速消耗。
合适的才是最好的 Balance算法 与Greedy算法不同的是,Kalyanasundaram和Pruhs提出的Balance算法忽略单个bidder的出价,尽可能平衡所有bidder的预算消耗,使得其在线时间尽可能⻓,即尽量使得所有⼴告都保持匀速投放。其算法描述如下:
当一个满足一些定向广告的请求到达时: if 广告预算消耗完 { continue } else { 选择一个(消耗/预算)值最小的一个广告 } 相比贪心算法,Balance算法平衡所有广告的消耗速度,能够有效解决贪心算法广告快速消耗的问题,但在广告消耗不尽的问题上依旧不是最佳解决方案。我们看下面特殊例子:
广告 出价($) 预算($) 定向 A 1 100 男,游戏 B 0.</description>
</item>
<item>
<title>URL中的空格、加号究竟应该使用何种方式编码</title>
<link>https://isites.github.io/timeline/http-url/</link>
<pubDate>Sun, 10 Oct 2021 12:20:38 +0800</pubDate>
<guid>https://isites.github.io/timeline/http-url/</guid>
<description>URL中不能显示地包含空格这已经是一个共识,而空格以何种形式存在,在不同的标准中又不完全一致,以致于不同的语言也有了不同的实现。
rfc2396中明确表示空格应该被编码为%20。
而W3C的标准中却又说空格可以被替换为+或者%20。
老许当场懵逼,空格被替换为+,那+本身只能被编码。既然如此,为什么不直接对空格进行编码呢。当然这只是老许心中的疑惑,以前的背景我们已经无法追溯,已成的事实我们也无法改变。但,空格到底是被替换为+还是20%,+是否需要被编码都是现在的我们需要直面的问题。
Go常用的三种URL编码方式 作为Gopher最先关注的自然是Go语言本身的实现,因此我们首先了解一下Go中常用的三种URL编码方式的异同。
url.QueryEscape fmt.Println(url.QueryEscape(&#34; +Gopher指北&#34;)) // 输出:+%2BGopher%E6%8C%87%E5%8C%97 使用url.QueryEscape编码时,空格被编码为+,而+本身被编码为%2B。
url.PathEscape fmt.Println(url.PathEscape(&#34; +Gopher指北&#34;)) // 输出:%20+Gopher%E6%8C%87%E5%8C%97 使用url.PathEscape编码时,空格被编码为20%, 而+则未被编码。
url.Values var query = url.Values{} query.Set(&#34;hygz&#34;, &#34; +Gopher指北&#34;) fmt.Println(query.Encode()) // 输出:hygz=+%2BGopher%E6%8C%87%E5%8C%97 使用(Values).Encode方法编码时,空格被编码为+,而+本身被编码为%2B,进一步查看(Values).Encode方法的源码知其内部仍旧调用url.QueryEscape函数。而(Values).Encode方法和url.QueryEscape的区别在于前者仅编码query中的key和value,后者会对=、&amp;均进行编码。
对我们开发者而言,这三种编码方式到底应该使用哪一种,请继续阅读后文相信你可以在后面的文章中找到答案。
不同语言中的实现 既然空格和+在Go中的URL编码方式有不同的实现,那在其他语言中是否也存在这样的情况呢,下面以PHP和JS为例。
PHP中的URL编码 urlencode
echo urlencode(&#39; +Gopher指北&#39;); // 输出:+%2BGopher%E6%8C%87%E5%8C%97 rawurlencode
echo rawurlencode(&#34; +Gopher指北&#34;); // 输出:%20%2BGopher%E6%8C%87%E5%8C%97 PHP的urlencode和Go的url.QueryEscape函数效果一致,而rawurlencode则将空格和+均进行编码。
JS中的URL编码 encodeURI
encodeURI(&#39; +Gopher指北&#39;) // 输出:%20+Gopher%E6%8C%87%E5%8C%97 encodeURIComponent
encodeURIComponent(&#39; +Gopher指北&#39;) // 输出:%20%2BGopher%E6%8C%87%E5%8C%97 JS的encodeURI和Go的url.PathEscape函数效果一致,而encodeURIComponent则将空格和+均进行编码。
我们应该怎么做 更推荐使用url.PathEscape函数编码 在前文中已经总结了Go、PHP和JS对 +Gopher指北的编码操作,下面总结一下其对应的解码操作是否可行的二维表。
编码/解码 url.QueryUnescape url.PathUnescape urldecode rawurldecode decodeURI decodeURIComponent url.</description>
</item>
<item>
<title>sync.Once化作一道光让我顿悟</title>
<link>https://isites.github.io/timeline/sync-once/</link>
<pubDate>Mon, 20 Sep 2021 12:20:38 +0800</pubDate>
<guid>https://isites.github.io/timeline/sync-once/</guid>
<description>前几天和公司同事吃饭直接社死,同事直言我写的文章很骚。
他们怎么知道我在写公众号!
我tm没在公众号里写什么奇奇怪怪的东西吧!
求求不要让公司更多同事知道这件事了!
大地为什么还没有裂开一条缝&hellip;
当时的心情历历在目,而老许此刻写下的文字却是另外一种想法。肤浅!简直太肤浅了!不要只浮于文字本身的魅力,请多关注老许分享的知识点(手动狗头)。另外一方面,老许觉得他们通过文章对我的认知有违我在公司树立的老实本分人设,但请不要奇怪也不要声张,毕竟我就是大部分程序员的缩影——“沉默寡言,心有一片海”。
我们高中物理老师常说,透过现象看本质,所以形式不重要,重要的是我想分享什么。这还要从一段有并发问题的代码说起(下面为公司部分源码简化后的模拟例子)。
type test struct { fff string } var resource *test func doSomething() error { if test == nil { n, e := rand.Int(rand.Reader, big.NewInt(3)) // 通过随机数模拟发生错误 if e != nil || n.Int64() &gt; 0 { retur fmt.Errorf(&#34;random [%w] err(%d)&#34;, e, n.Int64()) } // 未发生错误,则赋值 resource = &amp;test{&#34;关注公众号:Gopher指北&#34;} } // do something return nil } 老许微微一笑,这道题我会,反手利用sync.</description>
</item>
<item>
<title>一次带宽拉满引发的百分百超时血案!</title>
<link>https://isites.github.io/timeline/net-bandwidth/</link>
<pubDate>Sun, 05 Sep 2021 12:20:38 +0800</pubDate>
<guid>https://isites.github.io/timeline/net-bandwidth/</guid>
<description>偈语: 未经他人苦,莫劝他人善
鏖战两周有余,为了排查线上某接口百分百超时的原因,如今总算有些成果。虽然仍有疑虑但是碍于时间不允许和个人能力问题先做如下总结以备来日再战。
出口带宽拉满 能够发现这个问题实属侥幸。依稀记得这是一个风雨交加的夜晚,这风、这雨注定了今夜的不平凡。果然线上百分百超时的根因被发现了!
我们的线上接口需要对外请求,而我们的流出带宽被拉满自然耗时就长因此导致超时。当然这都是结果,毕竟中间过程的艰辛已经远远超出老许的文字所能描述的范围。
反思 结果有了,该有的反思仍旧不能少。比如流出带宽被拉满为什么没有提前预警!无论是自信带宽足够还是经验不足都值得老许记上一笔。
而在带宽问题被真正发现之前,老许内心对带宽其实已有所怀疑,但是却没有认真进行验证,只听信了他人的推测导致发现问题的时间被推迟。
httptrace 有时候不得不吹一波Go对http trace的良好支持。老许也是基于此做了一个demo,该demo可以打印http请求各阶段耗时。
上述为一次http请求各阶段耗时输出,有兴趣的可去https://github.com/Isites/go-coder/blob/master/httptrace/trace.go拿到源码。
老许对带宽的怀疑主要就是基于此demo中的源码进行线上分析测试给到的推测。
框架问题 本部分更加适合腾讯系的兄弟们去阅读,其他非腾讯系技术可以直接跳过。
我司的框架为TarsGo,我们在线上设置handletimeout为1500ms,该参数主要用于控制某一接口总耗时不超过1500ms,而我们的超时告警均为3s,因此即使带宽已满这个百分百超时告警也不应出现。
为了研究这个原因,老许只好花些零碎的时间去阅读源码,最终发现了TarsGo@v1.1.6的handletimeout控制是无效的。
下面看一下有问题的源码:
func (s *TarsProtocol) InvokeTimeout(pkg []byte) []byte { rspPackage := requestf.ResponsePacket{} rspPackage.IRet = 1 rspPackage.SResultDesc = &#34;server invoke timeout&#34; return s.rsp2Byte(&amp;rspPackage) } 当某接口总执行时间超过handletimeout时,会调用InvokeTimeout方法告知client调用超时,而上述的逻辑中忽略了IRequestId的响应,这就导致client收到响应包时无法将响应包和某次的请求对应起来,从而导致客户端一直等待响应直至超时。
最终修改如下:
func (s *TarsProtocol) InvokeTimeout(pkg []byte) []byte { rspPackage := requestf.ResponsePacket{} // invoketimeout need to return IRequestId reqPackage := requestf.RequestPacket{} is := codec.</description>
</item>
<item>
<title>公司内一次分享-Go并发编程</title>
<link>https://isites.github.io/timeline/go-share/</link>
<pubDate>Tue, 17 Aug 2021 21:00:00 +0800</pubDate>
<guid>https://isites.github.io/timeline/go-share/</guid>
<description>提到并发编程,不得不想到Go并发编程中的一句经典名言
Do not communicate by sharing memory; instead, share memory by communicating. 本次分享目标 避免Go并发编程一些常见的坑 理解为什么Go原生网络编程模型为什么这么爽 并发编程踩坑目录 优雅的代码不好定义,每位开发也有自己的风格,但是坑总是相似的。
channel误用 Case-1
func main() { wg := sync.WaitGroup{} ch := make(chan int, 10) for i := 0; i &lt; 10; i++ { ch &lt;- i // put task into channel } close(ch) wg.Add(4) for j := 0; j &lt; 4; j++ { go func() { for { task := &lt;-ch // do sth fmt.</description>
</item>
<item>
<title>惊!Go里面居然有这样精妙的小函数!</title>
<link>https://isites.github.io/timeline/fns/</link>
<pubDate>Tue, 03 Aug 2021 21:00:00 +0800</pubDate>
<guid>https://isites.github.io/timeline/fns/</guid>
<description>各位哥麻烦腾个道,前面是大型装逼现场。
首先老许要感谢他人的认同,这是我乐此不彼的动力,同时我也需要反思。这位小姐姐还是比较委婉, 但用我们四川话来说,前一篇文章的标题是真的cuo。
老许反复思考后决定哗众取宠一波,感叹号双连取名曰“惊!Go里面居然有这样精妙的小函数!”。下面就让我们来看看和标题没那么符合的一些小函数。
返回a/b向上舍入最接近的整数
func divRoundUp(n, a uintptr) uintptr { return (n + a - 1) / a } 这个方法用过的人应该不少,最典型的就是分页计算。
判断x是否为2的n次幂
func isPowerOfTwo(x uintptr) bool { return x&amp;(x-1) == 0 } 这个也挺容易理解的,唯一需要注意的是x需要大于0,因为该等式0也是成立的。
向上/下将x舍入为a的倍数,且a必须是2的n次幂
// 向上将x舍入为a的倍数,例如:x=6,a=4则返回值为8 func alignUp(x, a uintptr) uintptr { return (x + a - 1) &amp;^ (a - 1) } // 向上将x舍入为a的倍数,例如:x=6,a=4则返回值为4 func alignDown(x, a uintptr) uintptr { return x &amp;^ (a - 1) } 在这里老许再次明确一个概念,2的n次幂即为1左移n位。然后上述代码中^为单目运算法按位取反,则^ (a - 1)的运算结果是除了最低n位为0其余位全为1。剩余的部分则是一个简单的加减运算以及按位与。</description>
</item>
<item>
<title>讲的是切片,但好像又不只是切片?</title>
<link>https://isites.github.io/timeline/slice/</link>
<pubDate>Mon, 26 Jul 2021 22:09:00 +0800</pubDate>
<guid>https://isites.github.io/timeline/slice/</guid>
<description>我内心一直有一个欲望,想要高声呼喊“我胡汉三又回来了”,而现在就是合适的时机。
正式开干之前有点手生,太久没有写技术类的文章,总有点怠惰,不得不说坚持确实是一件很难的事情。如果不是因为愧疚和写点东西能让自己稍微平静下来一些,我可能还将继续怠惰下去。
另外还有一件很有意思的事情分享一下。前一篇在公众号上的文章仅思考就花了近一个月,写只花了一天,而技术文章我一般边思考边写平均耗时一周。结果是不会骗人的,前一篇文章阅读量首次突破一千,果然这届读者的思想深度至少也有一个月那么多,老许佩服佩服。
切片底层结构 切片和结构体的互转 其他不扯多了,我们还是回归本篇主题。 在正式了解切片底层结构之前, 我们先看几行代码。
type mySlice struct { data uintptr len int cap int } s := mySlice{} fmt.Println(fmt.Sprintf(&#34;%+v&#34;, s)) // {data:0 len:0 cap:0} s1 := make([]int, 10) s1[2] = 2 fmt.Println(fmt.Sprintf(&#34;%+v, len(%d), cap(%d)&#34;, s1, len(s1), cap(s1))) // [0 0 2 0 0 0 0 0 0 0], len(10), cap(10) s = *(*mySlice)(unsafe.Pointer(&amp;s1)) fmt.Println(fmt.Sprintf(&#34;%+v&#34;, s)) // {data:824634515456 len:10 cap:10} fmt.Printf(&#34;%p, %v\n&#34;, s1, unsafe.</description>
</item>
<item>
<title>天涯未远,上海再见,百度再见</title>
<link>https://isites.github.io/timeline/leave-sh/</link>
<pubDate>Sun, 06 Jun 2021 10:59:48 +0800</pubDate>
<guid>https://isites.github.io/timeline/leave-sh/</guid>
<description>关于离开想说的都在标题里。
离开的原因有很多,但是老许并不打算在这里详细阐述。说的越多都是在反复鞭尸自己的无能,老许对此次的离开总结为两个字——“败犬”(并无意冒犯他人,仅仅是对自己的总结。如果有兴趣交流的可以在公众号后台私我)。
以下为老许近一个多月的反思,这份反思没有什么大道理,更没有什么必须记住的知识点,老许写下来只是希望和各位读者有一个平等交流的机会。
一个无解的行业现状 &ldquo;居安思危&rdquo;,古人诚不欺我。我自认为在百度工作期间还是成长了很多,但是难免有松懈的时候,因此总的算下来是以一种平缓的速度进步。很长一段时间内老许都满足于现状,直到在金三银四的浪潮里开始挣扎。
“没有进步应该下十八层地狱,进步的慢更是一种原罪”,这是老许不由自主的感叹。以leetcode为例,不刷个一两百道题仿佛都不好意思出去面试。不知道这是不是内卷的一种。如果是,那一定是这个行业的人越来越多造成的。在程序猿从业人口越来越多的情况下,已经身在其中的人真是一刻也不容松懈呀。
写下这一段我也很犹豫,总有散播焦虑的嫌疑,但事实上这就是我真实的感受。
我简直毫无自控力 我简直毫无自控力,这个感悟来自于接受offer之后,把“某音”又重新装了回来。为什么会存在把它卸载这个事情呢?源自于开始找工作之后发现它严重影响了我的复习时间。明明知道应该把大量时间花在复习上,却总是不知不觉间刷起了短视频。虽然最近又把“某音”卸载了,但是又开始沉迷于某小破站。果然自控力对我来说就是扯淡。
和我等凡人不同,有些人明明已经成为了神,还是以极强的自控力给自己制定各种OKR和学习计划。看到这类人的时候,老许充分意识到作为一名凡夫俗子还是有点好高骛远了。他们欲带皇冠必承其重,我也不奢求自己有超强的自控力,只要在主次不颠倒的情况下,劳逸结合,按照合理的节奏前进即可,为达这个目的哪怕是以卸载这种强硬手段。
关于学习的反思 很早以前有人告诉我,“好记性不如烂笔头,但是最终你依旧要把它记在脑子里面才行”。这句话我深以为然,也是一直如此践行着。因此很多东西我做笔记也只是为了记住,而今终于自食恶果。
相信很多人都有这样一种感觉,年纪越大记忆力越来越差。我之前对某些知识点记忆不熟,也归咎于这个原因。我虽然做了笔记,但是没有对知识点做一个系统性的梳理,因此在我脑海中的知识点是杂乱无序的,自然忘的快。
幸运的是,我周边有这样的人在记忆知识点时会利用脑图做系统性的梳理。发现这一点时我幡然醒悟,我以前一直没有用一个有效的方法去记知识点。这个方法并不是什么秘密,很多人都知道但是并没有去使用。我相信这些人都和我一样只是缺少一个鹈鹕灌顶大彻大悟的契机。因此,我发自内心的感谢和我相遇的每一个人。
两个人的知识点不可能完全重合,因为你不可能看完所有的书,也不可能遇到他人遇到的所有实际问题,所以除了认真学习,获取知识点的另外一种有效途径就是交流和分享
对未来的规划和选择 这一段心里想了很多很多,有关于公司的选择,有关于未来的发展计划,但这些我自己也充满了不自信,所以就不在这里献丑了。
老许继续写下这段的原因是提醒自己现在的我还有选择的权利,如果有一天连选择的权利都没有了才是真正的穷途末路。虽然对未来依旧忐忑,但对老许来说一定会坚持的三件事是运动、挣钱和学习。相信这对所有人来说都是一样的(仿佛一句废话)。
最后,很庆幸有这样的一段时间,让自己重新思考,或许前路依旧迷雾重重,但依旧要砥砺前行。
最后的最后,感谢这一路上遇见的人和事。
【关注公众号】</description>
</item>
<item>
<title>有趣!一行代码居然无法获取请求的完整URL</title>
<link>https://isites.github.io/timeline/get-full-url/</link>
<pubDate>Mon, 29 Mar 2021 19:36:38 +0800</pubDate>
<guid>https://isites.github.io/timeline/get-full-url/</guid>
<description>缘起 做Web服务的时候,可能会有这样一个业务场景,获取一个HTTP请求的完整URL。很巧,老许就碰到了这样的业务场景。面对如此简单的需求,CV大法根本没有展示才能的机会。啪啪啪,获取请求的完整URL代码就出来了。
当时离验证只差一步,老许信心满满,很快,打脸来得很快就像龙卷风。。。
从图中可以知道,req.URL中的Scheme和Host均为空,所以r.URL.String()无法得到完整的请求连接。这个结果让老许一阵激动,万万没想到有一天我也有机会发现Go源码中可能遗漏的赋值。老许强行按耐住心中的激动,准备好好研究一番,万一成为了Go的Contributor呢^ω^。最后发现官方实现没有问题,因此就有了今天这篇文章。
HTTP1.1中为什么无法获取完整的连接 HTTP1.1的Server读取请求并构建Request.URL对象的逻辑在request.go文件的readRequest方法中,下面老许对其源码做一个简单分析总结。
读取请求的第一行,HTTP请求的第一行又称为请求行。 // First line: GET /index.html HTTP/1.0 var s string if s, err = tp.ReadLine(); err != nil { return nil, err } 将请求行的内容分别解析为req.Method、req.RequestURI和req.Proto。 var ok bool req.Method, req.RequestURI, req.Proto, ok = parseRequestLine(s) 将req.RequestURI解析为req.URL。 rawurl := req.RequestURI if req.URL, err = url.ParseRequestURI(rawurl); err != nil { return nil, err } 注:当请求方法是CONNECT时,上述流程略有变化
通过上面的流程我们知道req.URL的数据来源为req.RequestURI,而req.RequestURI到底是什么让我们继续阅读后文。
请求资源 根据rfc7230中的定义, 请求行分为请求方法、请求资源和HTTP版本,分别对应上述的req.Method、req.RequestURI和req.Proto(request-target在本文均被译作请求资源)。
关于请求方法有哪些想必不用老许在这儿科普了吧。至于常用的HTTP版本无非就是HTTP1.1和HTTP2。 下面主要介绍请求资源的几种形式。</description>
</item>
<item>
<title>白话Go内存模型&Happen-Before</title>
<link>https://isites.github.io/timeline/go-memory-model/</link>
<pubDate>Thu, 04 Mar 2021 18:53:38 +0800</pubDate>
<guid>https://isites.github.io/timeline/go-memory-model/</guid>
<description>Go内存模型明确指出,一个goroutine如何才能观察到其他goroutine对同一变量的写操作。
当多个goroutine并发同时存取同一个数据时必须把并发的存取操作序列化。在Go中保证读写的序列化可以通过channel通信或者其他同步原语(例如sync包中的互斥锁、读写锁和sync/atomic中的原子操作)。
Happens Before 在单goroutine中,读取和写入的行为一定是和程序指定的执行顺序表现一致。换言之,编译器和处理器在不改变语言规范所定义的行为前提下才可以对单个goroutine中的指令进行重排序。
a := 1 b := 2 由于指令重排序,b := 2可能先于a := 1执行。单goroutine中,该执行顺序的调整并不会影响最终结果。但多个goroutine场景下可能就会出现问题。
var a, b int // goroutine A go func() { a := 5 b := 1 }() // goroutine B go func() { for b == 1 {} fmt.Println(a) }() 执行上述代码时,预期goroutine B能够正常输出5,但因为指令重排序,b := 1可能先于a := 5执行,最终goroutine B可能输出0。
注:上述例子是个不正确的示例,仅作说明用。
为了明确读写的操作的要求,Go中引入了happens before,它表示执行内存操作的一种偏序关系。
happens-before的作用 多个goroutine访问共享变量时,它们必须建立同步事件来确保happens-before条件,以此确保读能够观察预期的写。
什么是Happens Before 如果事件e1发生在事件e2之前,那么我们说e2发生在e1之后。 同样,如果e1不在e2之前发生也没有在e2之后发生,那么我们说e1和e2同时发生。
在单个goroutine中,happens-before的顺序就是程序执行的顺序。那happens-before到底是什么顺序呢?我们看看下面的条件。
如果对于一个变量v的读操作r和写操作w满足下述两个条件,r才允许观察到w:</description>
</item>
<item>
<title>1分钟内的Linux性能分析法</title>
<link>https://isites.github.io/timeline/60-linux/</link>
<pubDate>Mon, 01 Feb 2021 20:30:38 +0800</pubDate>
<guid>https://isites.github.io/timeline/60-linux/</guid>
<description>本着“拿来主义”的精神,吸收他人长处为己用。老许翻译一篇Linux性能分析相关的文章分享给各位读者,同时也加深自己的印象。
你登录到具有性能问题的Linux服务器时,第一分钟要检查什么?
在Netflix,我们拥有庞大的Linux EC2云实例,以及大量的性能分析工具来监视和调查它们的性能。这些工具包括Atlas和Vector。Atlas用于全云监控,Vector用于按需实例分析。这些工具能帮助我们解决大部分问题,但有时候我们仍需登录实例并运行一些标准的Linux性能工具。
Atlas:根据github上面的文档老许简单说一下自己的认知。一个可以管理基于时间维度数据的后端,同时具有内存存储功能可以非常快速地收集和报告大量指标。
Vector:Vector是一个主机上的性能监视框架,它可以将各种指标展示在工程师的浏览器上面。
总结 在这篇文章中,Netflix性能工程团队将向您展示通过命令行进行性能分析是,前60秒应该使用那些Linux标准工具。在60秒内,你可以通过以下10个命令来全面了解系统资源使用情况和正在运行的进程。首先寻找错误和饱和指标,因为他们很容易理解,然后是资源利用率。饱和是指资源负载超出其处理能力,其可以表现为一个请求队列的长度或者等待时间。
uptime dmesg | tail vmstat 1 mpstat -P ALL 1 pidstat 1 iostat -xz 1 free -m sar -n DEV 1 sar -n TCP,ETCP 1 top 其中一些命令需要安装sysstat软件包。这些命令暴露的指标是一种帮助你完成USE Method(Utilization Saturation and Errors Method)——一种查找性能瓶颈的方法。这涉及检查所有资源(CPU、内存、磁盘等)利用率,饱和度和错误等指标。同时还需注意通过排除法可以逐步缩小资源检查范围。
以下各节通过生产系统中的示例总结了这些命令。这些命令的更多信息,请参考使用手册。
uptime $ uptime 23:51:26 up 21:31, 1 user, load average: 30.02, 26.43, 19.02 这是一种快速查看平均负载的方法,它指示了等待运行的进程数量。在Linux系统上,这些数字包括要在CPU上运行的进程以及处于I/O(通常是磁盘I/O)阻塞的进程。这提供了资源负载的大概状态,没有其他工具就无法理解更多。仅值得一看。
这三个数字分别代表着1分钟、5分钟和15分钟内的平均负载。这三个指标让我们了解负载是如何随时间变化的。例如,你被要求检查有问题的服务器,而1分钟的值远低于15分钟的值,则意味着你可能登录的太晚而错过了问题现场。
在上面的例子中,最近的平均负载增加,一分钟值达到30,而15分钟值达到19。数字如此之大意味着很多:可能是CPU需求(可以通过后文中介绍的vmstat或mpstat命令来确认)。
dmesg | tail $ dmesg | tail [1880957.563150] perl invoked oom-killer: gfp_mask=0x280da, order=0, oom_score_adj=0 [.</description>
</item>
<item>
<title>Go中的SSRF攻防战</title>
<link>https://isites.github.io/timeline/go-ssrf/</link>
<pubDate>Tue, 19 Jan 2021 20:30:38 +0800</pubDate>
<guid>https://isites.github.io/timeline/go-ssrf/</guid>
<description>写在最前面 “年年岁岁花相似,岁岁年年人不同”,没有什么是永恒的,很多东西都将成为过去式。比如,我以前在文章中自称“笔者”,细细想来这个称呼还是有一定的距离感,经过一番深思熟虑后,我打算将文章中的自称改为“老许”。
关于自称,老许就不扯太远了,下面还是回到本篇的主旨。
什么是SSRF SSRF英文全拼为Server Side Request Forgery,翻译为服务端请求伪造。攻击者在未能取得服务器权限时,利用服务器漏洞以服务器的身份发送一条构造好的请求给服务器所在内网。关于内网资源的访问控制,想必大家心里都有数。
上面这个说法如果不好懂,那老许就直接举一个实际例子。现在很多写作平台都支持通过URL的方式上传图片,如果服务器对URL校验不严格,此时就为恶意攻击者提供了访问内网资源的可能。
“千里之堤,溃于蚁穴”,任何可能造成风险的漏洞我们程序员都不应忽视,而且这类漏洞很有可能会成为别人绩效的垫脚石。为了不成为垫脚石,下面老许就和各位读者一起看一下SSRF的攻防回合。
回合一:千变万化的内网地址 为什么用“千变万化”这个词?老许先不回答,请各位读者耐心往下看。下面,老许用182.61.200.7(www.baidu.com的一个IP地址)这个IP和各位读者一起复习一下IPv4的不同表示方式。
格式 值 描述 点分十进制 182.61.200.7 常规表现方式 点分八进制 0266.075.0310.07 每个字节被单独转换为八进制 点分十六进制 0xb6.0x3d.0xc8.0x7 每个字节被单独转换为十六进制 十进制 3057502215 用十进制写出的32位整数 八进制 026617344007 用八进制写出32位整数 十六进制 0xb63dc807 用十六进制写出32位整数 点分混合制(4) 182.0x3d.0310.7等 点分格式中,每个字节都可用任意的进制表达 点分混合制(3) 182.0x3d.0144007等 将后面16位用八进制表示 点分混合制(2) 182.4048903等 将后面24为用10进制表示 注意⚠️:点分混合制中,以点分割地每一部分均可以写作不同的进制(仅限于十、八和十六进制)。
上面仅是IPv4的不同表现方式,IPv6的地址也有三种不同表示方式。而这三种表现方式又可以有不同的写法。下面以IPv6中的回环地址0:0:0:0:0:0:0:1为例。</description>
</item>
<item>
<title>2020总结:稍微努力了一下,依旧是咸鱼一条</title>
<link>https://isites.github.io/timeline/2020summary/</link>
<pubDate>Thu, 31 Dec 2020 00:12:48 +0800</pubDate>
<guid>https://isites.github.io/timeline/2020summary/</guid>
<description>2020最深刻的印记是新冠疫情,于笔者最深刻的印记是在这一年下定决心做了一些新的尝试。既然有了新的尝试,笔者也就随大流来一发年终总结。
关于“新世界杂货铺”这个名字 2020的年终总结,最想谈的就是新世界杂货铺这个名字。为什么想先谈名字呢?因为笔者最近正在思考要不要换个名字,毕竟这个名字和公众号的内容实在没什么直接联系。笔者一度怀疑就是因为这个名字才导致笔者的粉丝增长缓慢。
先说一说为什么叫这个名字吧。专注于技术分享但是又不止于技术分享故谓之“杂货铺”,比如笔者曾经深夜偷偷发了一篇“7年,爷青结,感谢春物的陪伴!”这样的文章,以后只会更多!对笔者而言,做公众号是一次崭新的尝试所以最终才取名为“新世界杂货铺”。名字的由来差不多就是这样,至于明年要不要换个名字,笔者还在犹豫,姑且走一步看一步吧。
关于分享 毫无疑问,2020年的上半年算是废了,到下半年笔者才下定决心做出改变。而改变的第一步,笔者选择了百家号。
2020年7月5日,正式在百家号发布笔者的第一篇原创文章。在百家号发表几篇文章后,笔者差点丧失继续创作的动力(因为某些原因,笔者就不描述过多细节了)。直到2020年8月,笔者将主要创作平台移至公众号、简书和掘金等平台,才有了继续下去的动力。
说实话,持续原创输出十分不易,特别是冬天到了笔者就更加懒了,笔者坚持码下的每一个字对笔者而言都是自我挑战成功的硕果。截止目前,笔者已在公众号发布21篇原创,创作不易,但是笔者仍然想坚持下去,只为心中不迷茫!
关于粉丝 自己写的文章有几斤几两笔者还是比较清楚,所以对于粉丝数量十分佛系。
毕竟粉丝少,只能这样安慰自己,要不然为什么想换名字呢?果然,男人的嘴骗人的鬼!
粉丝数量能拿出手的就只有简书,截止目前已有5k+的粉丝量。
据笔者观察,这5k的粉丝几乎都是僵尸粉,估摸着是为了增加我在简书创作的动力才将我推送给新注册的用户。不管怎么样,这个粉丝量还是令笔者很舒心。这暗示已经非常明显了!
健康和体重 就健康而言笔者还是有些感触。去年年末,因为肾结石去了趟医院。每每回忆起当时的情形都令笔者十分后悔,后悔自己为什么不多喝水,为什么一直坐着,为什么不多运动!在此,特意提醒各位读者一定要多运动多喝水,少坐!
鉴于笔者曾经遭受了非人的折磨,所以从2020年初开始就坚持做俯卧撑,并买了站立办公椅。比较遗憾的是笔者从11月才开始记录俯卧撑的数量,截止到12月底两个月共完成2085个俯卧撑。
健康是一方面,另一方面笔者也希望自己能够跨入瘦子的行列。笔者还特意为自己设定了一个奖励,只要体重减到130就奖励自己一副贵一点的眼镜。然而都已经到年底了,这个目标还遥遥无期,果然每一个胖子都非一日之功。
看过的书 又到了晒书的时间,以下是笔者今年已经读完的书。
万万没想到笔者这种不常看书的人一年时间也能读完4本,由此可见不积跬步,无以至千里,古人诚不欺我。
这四本书中,笔者最喜欢第二本。该书全名为《好好吃饭:无须自控力,三观最正的瘦身指南》,只看书名相信各位读者就应该明白笔者为什么喜欢这本书了。对了,笔者一般都使用微信读书,欢迎各位道友加好友一起交流。
春暖花开,面朝2021 每一篇年终总结的最后都会立一些Flag,本篇亦不例外。
Flag1:一定要保持一个健康的身体,最好是能瘦下来,毕竟笔者的眼镜确实该换了。
Flag2:持续学习输出,粉丝也能持续增长就更好了。
鉴于笔者对自己有一个较为全面的认知,所以Flag就不立多了,多了也只是自己骗自己。
最后一口毒鸡汤 细细想来,2020于我来说和大部分人一样,稍微努力了一下,然而依旧是条咸鱼。工作没有什么变化,生活没有什么变化,经济状况还是没有什么变化。这是最好的结果,也是最坏的结果,或许有些时候真的要豁出去才能打破常规。
不矫情了,笔者最后祝愿所有人在新的一年里平淡且美好!
【关注公众号】</description>
</item>
<item>
<title>码了2000多行代码就是为了讲清楚TLS握手流程(续)</title>
<link>https://isites.github.io/timeline/gotls1.2/</link>
<pubDate>Sun, 13 Dec 2020 23:47:48 +0800</pubDate>
<guid>https://isites.github.io/timeline/gotls1.2/</guid>
<description>在“码了2000多行代码就是为了讲清楚TLS握手流程”这一篇文章的最后挖了一个坑,今天这篇文章就是为了填坑而来,因此本篇主要分析TLS1.2的握手流程。
在写前一篇文章时,笔者的Demo只支持解析TLS1.3握手流程中发送的消息,写本篇时,笔者的Demo已经可以解析TLS1.x握手流程中的消息,有兴趣的读者请至文末获取Demo源码。
结论先行 为保证各位读者对TLS1.2的握手流程有一个大概的框架,本篇依旧结论先行。
单向认证 单向认证客户端不需要证书,客户端验证服务端证书合法即可访问。
下面是笔者运行Demo打印的调试信息:
根据调试信息知,TLS1.2单向认证中总共收发数据四次,Client和Server从这四次数据中分别读取不同的信息以达到握手的目的。
笔者将调试信息转换为下述时序图,以方便各位读者理解。
双向认证 双向认证不仅服务端要有证书,客户端也需要证书,只有客户端和服务端证书均合法才可继续访问(笔者的Demo如何开启双向认证请参考前一篇文章中HTTPS双向认证部分)。
下面是笔者运行Demo打印的调试信息:
同单向认证一样,笔者将调试信息转换为下述时序图。
双向认证和单向认证相比,Server发消息给Client时会额外发送一个certificateRequestMsg消息,Client收到此消息后会将证书信息(certificateMsg)和签名信息(certificateVerifyMsg)发送给Server。
双向认证中,Client和Server发送的消息变多了,但是总的数据收发仍然只有四次。
总结 1、单向认证和双向认证中,总的数据收发仅四次(比TLS1.3多一次数据收发),单次发送的数据中包含一个或者多个消息。
2、TLS1.2中除了finishedMsg其余消息均未加密。
3、在TLS1.2中,ChangeCipherSpec消息之后的所有数据均会做加密处理,它的作用在TLS1.2中更像是一个开启加密的开关(TLS1.3中忽略此消息,并不做任何处理)。
和TLS1.3的比较 消息格式的变化 对比本篇的时序图和前篇的时序图很容易发现部分消息格式发生了变化。下面是certificateMsg和certificateMsgTLS13的定义:
// TLS1.2 type certificateMsg struct { raw []byte certificates [][]byte } // TLS1.3 type certificateMsgTLS13 struct { raw []byte certificate tls.Certificate ocspStapling bool scts bool } 其他消息的定义笔者就不一一列举了,这里仅列出格式发生变化的消息。
TLS1.2 TLS1.3 certificateRequestMsg certificateRequestMsgTLS13 certificateMsg certificateMsgTLS13 消息类型的变化 TLS1.</description>
</item>
<item>
<title>区分Protobuf 3中缺失值和默认值</title>
<link>https://isites.github.io/timeline/gopb3/</link>
<pubDate>Tue, 01 Dec 2020 12:20:38 +0800</pubDate>
<guid>https://isites.github.io/timeline/gopb3/</guid>
<description>这两天翻了翻以前的项目,发现不同项目中关于Protobuf 3缺失值和默认值的区分居然有好几种实现。今天笔者冷饭新炒,结合项目中的实现以及切身经验共总结出如下六种方案。
增加标识字段 众所周知,在Go中数字类型的默认值为0(这里仅以数字类型举例),这在某些场景下往往会引起一定的歧义。
以is_show字段为例,如果没有该字段表示不更新DB中的数据,如果有该字段且值为0则表示更新DB中的数据为不可见,如果有该字段且值为1则表示更新DB中的数据为可见。
上述场景中,实际要解决的问题是如何区分默认值和缺失字段。增加标识字段是通过额外增加一个字段来达到区分的目的。
例如:增加一个has_show_field字段标识is_show是否为有效值。如果has_show_field为true则is_show为有效值,否则认为is_show未设置值。
此方案虽然直白,但每次设置is_show的值时还需设置has_show_field的值,甚是麻烦故笔者十分不推荐。
字段含义和默认值区分 字段含义和默认值区分即不使用对应类型的默认值作为该字段的有效值。接着前面的例子继续描述,is_show为1时表示展示,is_show为2时表示不展示,其他情况则认为is_show未设置值。
此方案笔者还是比较认可的,唯一问题就是和开发者的默认习惯略微不符。
使用oneof oneof 的用意是达到 C 语言 union 数据类型的效果,但是诸多大佬还是发现它可以标识缺失字段。
message Status { oneof show { int32 is_show = 1; } } message Test { int32 bar = 1; Status st = 2; } 上述proto文件生成对应go文件后,Test.St为Status的指针类型,故通过此方案可以区分默认值和缺失字段。但是笔者认为此方案做json序列化时十分不友好,下面是笔者的例子:
// oneof to json ot1 := oneof.Test{ Bar: 1, St: &amp;oneof.Status{ Show: &amp;oneof.Status_IsShow{ IsShow: 1, }, }, } bts, err := json.</description>
</item>
<item>
<title>为什么go中的receiver name不推荐使用this或者self</title>
<link>https://isites.github.io/timeline/go-reciver/</link>
<pubDate>Sat, 29 Aug 2020 12:20:38 +0800</pubDate>
<guid>https://isites.github.io/timeline/go-reciver/</guid>
<description>前言 在日常的开发中我们除了定义函数以外, 我们还会定义一些方法。这本来没有什么, 但是一些从PHP或者其他面向对象语言转GO的同学往往会把receiver name命名为this, self, me等。
笔者在实际项目开发中也遇到类似的同学, 屡次提醒却没有效果,于是决心写下这篇文章以便好好说服这些同学。
CR标准做法 首先我们来看一下GO推荐的标准命名Receiver Names,以下内容摘抄自https://github.com/golang/go/wiki/CodeReviewComments#receiver-names:
The name of a method&#39;s receiver should be a reflection of its identity; often a one or two letter abbreviation of its type suffices (such as &#34;c&#34; or &#34;cl&#34; for &#34;Client&#34;). Don&#39;t use generic names such as &#34;me&#34;, &#34;this&#34; or &#34;self&#34;, identifiers typical of object-oriented languages that gives the method a special meaning. In Go, the receiver of a method is just another parameter and therefore, should be named accordingly.</description>
</item>
<item>
<title>深入剖析go中字符串的编码问题——特殊字符的string怎么转byte?</title>
<link>https://isites.github.io/timeline/go-string-encode/</link>
<pubDate>Mon, 24 Aug 2020 23:47:48 +0800</pubDate>
<guid>https://isites.github.io/timeline/go-string-encode/</guid>
<description>前言 前段时间发表了Go中的HTTP请求之——HTTP1.1请求流程分析,所以这两天本来打算研究HTTP2.0的请求源码,结果发现太复杂就跑去逛知乎了,然后就发现了一个非常有意思的提问“golang 特殊字符的string怎么转成[]byte?”。为了转换一下心情, 便有了此篇文章。
问题 原问题我就不码字了,直接上图: 看到问题,我的第一反应是ASCII码值范围应该是0~127呀,怎么会超过127呢?直到实际运行的时候才发现上图的特殊字符是‘’(如果无法展示,记住该特殊字符的unicode是\u0081),并不是英文中的句号。
unicode和utf-8的恩怨纠葛 百度百科已经把unicode和utf-8介绍的很详细了,所以这里就不做过多的阐述,仅摘抄部分和本文相关的定义:
Unicode为每个字符设定了统一并且唯一的二进制编码,通常用两个字节表示一个字符。 UTF-8是针对Unicode的一种可变长度字符编码。它可以用来表示Unicode标准中的任何字符。UTF-8的特点是对不同范围的字符使用不同长度的编码。对于0x00-0x7F之间的字符,UTF-8编码与ASCII编码完全相同。 go中的字符 众所周知,go中能表示字符的有两种类型,分别是byte和rune,byte和rune的定义分别是:type byte = uint8和type rune = int32。
uint8范围是0-255,只能够表示有限个unicode字符,超过255的范围就会编译报错。根据上述关于unicode的定义,4字节的rune完全兼容两字节的unicode。
我们用下面的代码来验证:
var ( c1 byte = &#39;a&#39; c2 byte = &#39;新&#39; c3 rune = &#39;新&#39; ) fmt.Println(c1, c2, c3) 上述的程序根本无法运行,因为第二行编译会报错,vscode给到了十分详细的提示:'新' (untyped rune constant 26032) overflows byte。
接下来,我们通过下面的代码来验证字符和unicode和整型的等价关系:
fmt.Printf(&#34;0x%x, %d\n&#34;, &#39;&#39;, &#39;&#39;) //输出:0x81, 129 fmt.Println(0x81 == &#39;&#39;, &#39;\u0081&#39; == &#39;&#39;, 129 == &#39;&#39;) // 输出:true true true //\u0081输出到屏幕上后不展示, 所以换了大写字母A来输出 fmt.</description>
</item>
</channel>
</rss>