-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathatom.xml
3455 lines (3286 loc) · 694 KB
/
atom.xml
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
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>存档Save&Load</title>
<subtitle>存档意味着放下包袱,搞砸了不过回来读档</subtitle>
<link href="/atom.xml" rel="self"/>
<link href="https://jaycechant.info/"/>
<updated>2022-01-23T06:58:10.132Z</updated>
<id>https://jaycechant.info/</id>
<author>
<name>Jayce Sigit Chant</name>
</author>
<generator uri="http://hexo.io/">Hexo</generator>
<entry>
<title>向 QT5 (C++) 程序传递编译期参数</title>
<link href="https://jaycechant.info/2022/pass-compile-time-arguments-to-qt5-cpp-program/"/>
<id>https://jaycechant.info/2022/pass-compile-time-arguments-to-qt5-cpp-program/</id>
<published>2022-01-16T09:17:11.000Z</published>
<updated>2022-01-23T06:58:10.132Z</updated>
<content type="html"><![CDATA[<p>记在一个遗留 C++ 项目上改造版本号。</p>
<a id="more"></a>
<h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>C++ 加 QT5 的历史遗留项目。彻底重写不实际,很多坑要重新踩。原作者找不着人,连问都没人可以问。只能重构梳理之后 fix bug 先凑合用。</p>
<p>琐碎的重构不值一提,唯独版本号的改造,可以记录一下,给以后参考。</p>
<h3 id="版本号更新"><a href="#版本号更新" class="headerlink" title="版本号更新"></a>版本号更新</h3><p>之前的版本号是写死在代码里的。想起来改一下,有时就忘了。</p>
<p>这样会有一些弊端:</p>
<ul>
<li><p>首先是增加记忆负担。</p>
<p> 修改版本号动作很小。但开发的粒度跟发布的粒度并不一致。提交的是最小可运行粒度。发布则还要考虑业务和运营。一次发布往往包含多个提交。提交的时候,未必能预计到发布的时机并修改版本号。</p>
</li>
<li><p>相对可行的办法,是每次发布完,马上改掉版本号,作为下次发布的预备。</p>
<p> 但这样提交历史里,会有大量版本号的提交,稀释了真正有用的提交。</p>
</li>
<li><p>何况即使这样,还是无法跟踪两次正式发布之间的测试版本。</p>
<p> 一般完整的版本号,会同时包含 供用户关注的、粒度较大的『发布版本号』,和 内部跟踪的、粒度更小的『内部版本号』。git 的 commit hash (SHA-1) 就是一个天然的内部版本号,帮我们把程序唯一对应到一个提交。有些时候还要加上系统平台、架构或编译器的信息。可一旦加上这些信息,上面的问题就更严重了。</p>
</li>
</ul>
<p>要么不记得,要么改不对,还污染历史。</p>
<p>这些矛盾的根源,在于 <strong>版本号主要作用于 测试、发布、运维,帮我们跟踪代码版本、构建信息,其内容和代码逻辑本身几乎是正交的,是在代码提交完之后才真正确定下来的;却为了后续的跟踪,偏偏需要内置在程序里</strong> 。</p>
<p>在代码里修改版本号,就成了当预言家,要预测版本什么时候发布,能不能通过测试,能不能正常发布,注定吃力不讨好。</p>
<p>如果版本号里还需要包含 commit hash,在提交前就预测并写入代码,则几乎是不可能的任务。信息摘要有雪崩效应,hash 由提交的所有信息共同确定;但是在这个提交里,却要先把 hash 写入。这就陷入了 鸡-蛋悖论。</p>
<h3 id="编译期确定"><a href="#编译期确定" class="headerlink" title="编译期确定"></a>编译期确定</h3><p>所以合理的做法是,代码里预留这些信息的位置和相关的代码,值等到代码提交后的某一刻再决定。一般来说,这个时刻就是编译的时候。</p>
<p>对于 Go 而言,就是在编译时,给 linker 传递 <code>-ldflags "-X '<包名>.<变量名>=<字符串>'"</code> ,就可以给代码里对应的(string)变量赋值。</p>
<p>举例说,如果在 <code>main.go</code> 里这样写</p>
<figure class="highlight go"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div></pre></td><td class="code"><pre><div class="line"><span class="keyword">package</span> main</div><div class="line"></div><div class="line"><span class="keyword">import</span> (</div><div class="line"> <span class="string">"fmt"</span></div><div class="line">)</div><div class="line"></div><div class="line"><span class="keyword">var</span> version <span class="keyword">string</span></div><div class="line"></div><div class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> {</div><div class="line"> fmt.Println(version)</div><div class="line">}</div></pre></td></tr></table></figure>
<p>编译时加上 <code>-ldflags "-X 'main.version=v1.2.3'"</code> ,那么我们最后运行程序得到的输出就是 <code>v1.2.3</code> 。</p>
<p>更具体地,我一般会在 <code>Makefile</code> 里这样写:</p>
<figure class="highlight makefile"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div></pre></td><td class="code"><pre><div class="line"><span class="section">build:</span></div><div class="line"> go build -ldflags <span class="string">"-X 'main.version=$(TAG)' -X 'main.goVersion=$(shell go version)' -X 'main.commitHash=$(shell git show -s --format=%H)' -X 'main.buildTime=$(shell date "</span>+%Y-%m-%d %T%z<span class="string">")'"</span></div></pre></td></tr></table></figure>
<p>主版本号通过命令行通过环境变量 <code>TAG</code> 传入,其它的信息则通过 shell 自动获取。</p>
<h2 id="C-和-QT5-下的方案"><a href="#C-和-QT5-下的方案" class="headerlink" title="C++ 和 QT5 下的方案"></a>C++ 和 QT5 下的方案</h2><p>我需要在 C++ 里做到类似的事情。</p>
<blockquote>
<p>特别强调,我本来用 C++ 就用得很浅,再加上已经有十年不怎么写,下面的内容仅仅对我而言值得记录一笔,对 C++ 高手来说可能完全不值一提。</p>
</blockquote>
<h3 id="D-参数"><a href="#D-参数" class="headerlink" title="-D 参数"></a>-D 参数</h3><p>项目用的是 g++ 编译器,可以很容易查到,最接近的做法,是给编译器传递 <code>-D<宏>[=值]</code> 来定义宏。</p>
<p>举例说,<code>-DDEBUG_LOG</code> 相当于 <code>#define DEBUG_LOG</code> ;<code>-DVERSION=v1.2.3</code> 则相当于 <code>#define VERSION v1.2.3</code> 。注意 <code>-D</code> 和后续内容中间没有任何间隔或者符号。</p>
<p>时间关系,我没有去确认其它编译器是否也有这个参数。</p>
<h3 id="qmake-和-pro"><a href="#qmake-和-pro" class="headerlink" title="qmake 和 .pro"></a>qmake 和 .pro</h3><p>然而我们并不会手动调用 <code>g++</code> ,甚至都不会手写 <code>Makefile</code> 。项目下确实是有一个 <code>Makefile</code> ,但它是 <code>qmake</code> 自动生成的,任何修改都会在后续被覆盖。<code>qmake</code> 从 <code><项目名>.pro</code> 生成 <code>Makefile</code> 。</p>
<p>结合现有的 <code>.pro</code> 文件和网上的信息,很容易确定 <code>DEFINES</code> 的值,最终会变成 <code>Makefile</code> 里的 <code>-D</code> 参数。</p>
<p>于是我在 <code>.pro</code> 加了一行</p>
<figure class="highlight makefile"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div></pre></td><td class="code"><pre><div class="line"><span class="comment"># git commit hash</span></div><div class="line">DEFINES += COMMIT_HASH=<span class="variable">$(shell git show -s --format=%h --abbrev=10)</span></div></pre></td></tr></table></figure>
<p>(暂时版本号里不想添加太多东西,只传入了 commit hash;甚至完整的 hash 都略嫌太长,只保留了前 10 位。)</p>
<p>迫不及待地开始编译,得到的 <code>Makefile</code> 里却是这样的</p>
<figure class="highlight makefile"><table><tr><td class="gutter"><pre><div class="line">1</div></pre></td><td class="code"><pre><div class="line">-DCOMMIT_HASH=$(shell -Dgit -Dshow -D-s -D--format=%h -D--abbrev=10)</div></pre></td></tr></table></figure>
<p>以空格为分隔符,每一段都成了一个参数……</p>
<p>不过这个很好解决,加个引号就行</p>
<figure class="highlight makefile"><table><tr><td class="gutter"><pre><div class="line">1</div></pre></td><td class="code"><pre><div class="line">DEFINES += COMMIT_HASH="<span class="variable">$(shell git show -s --format=%h --abbrev=10)</span>"</div></pre></td></tr></table></figure>
<p>引号也可以加在别的地方,只要把空格放在引号里,保证不会断开就行</p>
<figure class="highlight makefile"><table><tr><td class="gutter"><pre><div class="line">1</div></pre></td><td class="code"><pre><div class="line">DEFINES += "COMMIT_HASH=<span class="variable">$(shell git show -s --format=%h --abbrev=10)</span>"</div></pre></td></tr></table></figure>
<p>无论是哪一种,最后得到的 <code>Makefile</code> 里都是</p>
<figure class="highlight makefile"><table><tr><td class="gutter"><pre><div class="line">1</div></pre></td><td class="code"><pre><div class="line">-DCOMMIT_HASH=$(shell git show -s --format=%h --abbrev=10)</div></pre></td></tr></table></figure>
<p>有效信息已经从 <code>qmake</code> 和 <code>.pro</code> 传到了 <code>make</code> 和 <code>Makefile</code> 手里。接下来 <code>make</code> 执行时,会执行 <code>$(shell ...)</code> 里的内容,并将结果原地展开。那么从 <code>make</code> 传递给 <code>g++</code> 的时候,<code>-DCOMMIT_HASH=</code> 后面就已经是具体的值了。</p>
<p>相当于 <code>#define COMMIT_HASH 具体的hash</code> </p>
<h3 id="宏不是字符串"><a href="#宏不是字符串" class="headerlink" title="宏不是字符串"></a>宏不是字符串</h3><p>宏有了,我们终于来到代码部分。</p>
<p>由于宏是外部定义的,代码里没有,直接用会报错。只是不同 IDE 具体报的错不同。(因为 QtCreator 不好用,我在 VS Code 里写代码,QtCreator 只用来编译。)</p>
<p>即使不考虑这些报错,也可能存在后续忘了传对应参数的情况,从而导致代码有不可预料的结果。最好的办法还是加上 <code>#ifdef</code></p>
<figure class="highlight c++"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div></pre></td><td class="code"><pre><div class="line">QString version;</div><div class="line"><span class="meta">#<span class="meta-keyword">ifdef</span> COMMIT_HASH</span></div><div class="line"> version = COMMIT_HASH;</div><div class="line"><span class="meta">#<span class="meta-keyword">endif</span></span></div></pre></td></tr></table></figure>
<p>加了之后,VS Code 不再报错了(其实是因为它认为这个宏没有定义,直接忽略了这段代码)。但 QtCreator 很聪明地意识到还有问题,报了一个 <code>expected expression</code> (这里需要一个表达式)。而如果改为将 <code>COMMIT_HASH</code> 当做字符串参数用,会提示少了参数。</p>
<p>究其原因,是 <code>COMMIT_HASH</code> 只是一个宏,而且是一个外部定义的宏,不是字符串。</p>
<h3 id="外部定义"><a href="#外部定义" class="headerlink" title="外部定义"></a>外部定义</h3><p>到这里,对 C++ 已经很生疏的我有点懵,然后就开始胡乱地尝试。(接下来的病急乱投医让各位见笑了)</p>
<p>如果改为 <code>version = "COMMIT_HASH";</code> ,倒是不报错了,但是 <code>version</code> 得到的,也确实只有一个 <code>"COMMIT_HASH"</code> 的字符串。这是因为</p>
<blockquote>
<p>宏参数不会在字符串常量内部替换(parameters are not replaced inside string constants)。</p>
</blockquote>
<p>宏是预处理器负责处理的,早在开始编译之前就已经被替换。如果在 <code>.pro</code> 里再额外加了引号,试图让宏的值带上引号呢:</p>
<figure class="highlight makefile"><table><tr><td class="gutter"><pre><div class="line">1</div></pre></td><td class="code"><pre><div class="line">DEFINES += COMMIT_HASH="\"<span class="variable">$(shell git show -s --format=%h --abbrev=10)</span>\""</div></pre></td></tr></table></figure>
<p>相当于 <code>#define COMMIT_HASH "具体的hash"</code> 。然而没有用,仍然报错。</p>
<p>因为这种外部定义的宏,跟真实写在代码里的宏,还不一样。</p>
<p>实际在代码里定义一个 <code>COMMIT_HASH</code> 对比一下效果(以下报错都来自 VS Code):</p>
<ul>
<li><code>#define COMMIT_HASH 1234</code> ,引用处报错 <code>no suitable constructor exists to convert from "int" to "QString" : C/C++(415)</code>;</li>
<li><code>#define COMMIT_HASH 123aef</code> (没有 <code>0x</code> 前缀的 16进制数,模拟真实的 commit hash 值),报错 <code>user-defined literal operator not found : C/C++(2486)</code> ;</li>
<li><code>#define COMMIT_HASH "123aef"</code> ,可以正常编译。</li>
</ul>
<p>有这些差别,是因为写在代码里的宏定义,就在编译器和工具链 <strong>眼皮底下</strong> ,完全可以先展开,再做静态分析。</p>
<p>可对于外部定义的宏,这些分析就没法做。工具链无法确定这是什么东西,也就无法确定究竟是以上哪种情况。</p>
<h3 id="套娃的-展开"><a href="#套娃的-展开" class="headerlink" title="套娃的 # 展开"></a>套娃的 # 展开</h3><p>我们需要给工具链一个保证:在不知道宏的值的情况下,拿到的仍然是一个字符串。</p>
<p>对 C++ 来说,我们需要预处理运算符(stringizing operator):<code>#</code> ,它会把参数对应的字面文本转换为字符串。</p>
<p>是的,<code>#</code> 不仅是所有预处理指令(preprocessing directive)的开头,单独的 <code>#</code> 也是一个预处理运算符(preprocessing operator)。</p>
<p>为此我找了两个参考文档,分别来自</p>
<ul>
<li>GNU : <a href="https://gcc.gnu.org/onlinedocs/cpp/Stringizing.html" target="_blank" rel="external">https://gcc.gnu.org/onlinedocs/cpp/Stringizing.html</a></li>
<li>微软 : <a href="https://docs.microsoft.com/en-us/cpp/preprocessor/stringizing-operator-hash" target="_blank" rel="external">https://docs.microsoft.com/en-us/cpp/preprocessor/stringizing-operator-hash</a></li>
</ul>
<p>两边结合着理解。</p>
<h4 id="第一层:直接用"><a href="#第一层:直接用" class="headerlink" title="第一层:直接用"></a>第一层:直接用</h4><p>上来直接把代码改为 <code>version = #COMMIT_HASH;</code> ,得到的错误是 <code>'#' not expected here : C/C++(10)</code> 。</p>
<h4 id="第二层:函数宏"><a href="#第二层:函数宏" class="headerlink" title="第二层:函数宏"></a>第二层:函数宏</h4><p>结合例子理解一下,哦,貌似 <code>#</code> 只能在宏里面生效。好,改成这样</p>
<figure class="highlight c++"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div></pre></td><td class="code"><pre><div class="line"><span class="meta">#<span class="meta-keyword">define</span> STR(S) #S</span></div><div class="line"></div><div class="line"><span class="comment">//....</span></div><div class="line"></div><div class="line">QString version;</div><div class="line"><span class="meta">#<span class="meta-keyword">ifdef</span> COMMIT_HASH</span></div><div class="line"> version = STR(COMMIT_HASH);</div><div class="line"><span class="meta">#<span class="meta-keyword">endif</span></span></div></pre></td></tr></table></figure>
<p>再试一下。这回可以正常编译。可是运行之后发现,得到的还是一个 <code>"COMMIT_HASH"</code> 。</p>
<p>还是着急了,只看了文档开头就动手。还是要把文档看完。</p>
<p>咦,不对啊,为什么 GNU 的例子展开了。你看</p>
<figure class="highlight c++"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div></pre></td><td class="code"><pre><div class="line"><span class="meta">#<span class="meta-keyword">define</span> WARN_IF(EXP) \</span></div><div class="line">do { <span class="meta-keyword">if</span> (EXP) \</div><div class="line"> fprintf (stderr, <span class="meta-string">"Warning: "</span> #EXP <span class="meta-string">"\n"</span>); } \</div><div class="line">while (0)</div><div class="line"></div><div class="line">WARN_IF (x == <span class="number">0</span>);</div></pre></td></tr></table></figure>
<p>等价于</p>
<figure class="highlight c++"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div></pre></td><td class="code"><pre><div class="line"><span class="keyword">do</span> { <span class="keyword">if</span> (x == <span class="number">0</span>)</div><div class="line"> <span class="built_in">fprintf</span> (<span class="built_in">stderr</span>, <span class="string">"Warning: "</span> <span class="string">"x == 0"</span> <span class="string">"\n"</span>); } <span class="keyword">while</span> (<span class="number">0</span>);</div></pre></td></tr></table></figure>
<p>这 <code>EXP</code> 不是展开成 <code>x == 0</code> 了吗?</p>
<p>真的吗?</p>
<p>这里暂停让读者自己理一下。</p>
<p>1</p>
<p>2</p>
<p>3</p>
<p>4</p>
<p>5</p>
<p>哎呀,把自己绕进去了。让我们一一对应一下:</p>
<ul>
<li><code>EXP</code> 对应 <code>S</code> ;</li>
<li>展开之后 <code>x == 0</code> 对应 <code>COMMIT_HASH</code> ;</li>
<li>字符串里,<code>x</code> 没有进一步展开,<code>COMMIT_HASH</code> 也没有。</li>
</ul>
<p>好吧,确实没错。</p>
<blockquote>
<p><code>EXP</code> 的参数会 <strong>原封不动</strong> 替换到 if 语句中,并且被字符串化成为 <code>fprintf</code> 的参数。如果 <code>x</code> 是一个宏,它会在 if 语句中继续展开,但在字符串中不会。</p>
<p>(The argument for EXP is substituted once, <strong>as-is</strong>, into the if statement, and once, stringized, into the argument to fprintf. If x were a macro, it would be expanded in the if statement, but <strong>not in the string</strong>.)</p>
</blockquote>
<p>看到这里我懂了,<code>#</code> 的字符串化 <strong>先起作用</strong>。然后 <strong>双引号阻止了里面的宏进一步展开</strong>。</p>
<h4 id="第三层:套娃"><a href="#第三层:套娃" class="headerlink" title="第三层:套娃"></a>第三层:套娃</h4><p>那我就是需要继续展开怎么办?</p>
<p>其实两边文档后面都给出了例子。还是以 GNU 的来参考(毕竟我们用的 g++):</p>
<figure class="highlight c++"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div></pre></td><td class="code"><pre><div class="line"><span class="meta">#<span class="meta-keyword">define</span> xstr(s) str(s)</span></div><div class="line"><span class="meta">#<span class="meta-keyword">define</span> str(s) #s</span></div><div class="line"><span class="meta">#<span class="meta-keyword">define</span> foo 4</span></div><div class="line"></div><div class="line">str (foo)</div><div class="line">xstr (foo)</div></pre></td></tr></table></figure>
<p><code>str (foo)</code> 对应的展开是 <code>"foo"</code> , <code>xstr (foo)</code> 则经历了两次展开:</p>
<ul>
<li>第一步先展开成 <code>str(4)</code> ,</li>
<li>第二步再展开成 <code>"4"</code> 。</li>
</ul>
<p>其实就是多展开一次,让 <code>#</code> 不那么快生效,好让传进去的宏有机会展开。</p>
<p>果然,我的代码改为</p>
<figure class="highlight c++"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div></pre></td><td class="code"><pre><div class="line"><span class="meta">#<span class="meta-keyword">define</span> STR(S) #S</span></div><div class="line"><span class="meta">#<span class="meta-keyword">define</span> VSTR(V) STR(V)</span></div><div class="line"></div><div class="line"><span class="comment">//....</span></div><div class="line"></div><div class="line">QString version;</div><div class="line"><span class="meta">#<span class="meta-keyword">ifdef</span> COMMIT_HASH</span></div><div class="line"> version = VSTR(COMMIT_HASH);</div><div class="line"><span class="meta">#<span class="meta-keyword">endif</span></span></div></pre></td></tr></table></figure>
<p>就达到了目的。到这里,运行之后,<code>version</code> 输出的就是具体的 git commit hash 了。</p>
<h4 id="一点扩展"><a href="#一点扩展" class="headerlink" title="一点扩展"></a>一点扩展</h4><p>这里有了一个问题,如果传进去的宏,里面还有一层宏呢?</p>
<p>以 GNU 的例子为例,如果变成这样</p>
<figure class="highlight c++"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div></pre></td><td class="code"><pre><div class="line"><span class="meta">#<span class="meta-keyword">define</span> xstr(s) str(s)</span></div><div class="line"><span class="meta">#<span class="meta-keyword">define</span> str(s) #s</span></div><div class="line"></div><div class="line"><span class="meta">#<span class="meta-keyword">define</span> foo bar</span></div><div class="line"><span class="meta">#<span class="meta-keyword">define</span> bar 4</span></div><div class="line"></div><div class="line">xstr (foo)</div></pre></td></tr></table></figure>
<p>会得到什么?</p>
<p>是 <code>"bar"</code> 还是 <code>"4"</code> ?</p>
<p>函数宏的层数需要总是比参数宏多一层吗?</p>
<p>需要再定义一层 <code>#define ystr(s) xstr(s)</code> 吗?</p>
<p>还说说两层的函数宏就能应对所有情况?</p>
<p>这个问题就留给读者自己尝试吧。</p>
<p>欢迎在留言说说你的看法。</p>
<h2 id="后记"><a href="#后记" class="headerlink" title="后记"></a>后记</h2><p>我有十年不怎么写过 C++ 了。</p>
<p>C++ 性能好,可以访问底层接口,可以精确控制代码行为和内存分配,支持多范式,开发自由度大,优点很多。所以在很多领域是首选。</p>
<p>但真的太复杂了,新手容易玩脱,老手把代码写对也不算轻松。心智负担大,历史包袱重。</p>
<p>要获得它那些优点,我甚至宁可写 C。其它场景,也优先 选择 Java,或者 Go。反正我是不可能选 C++ 作为新项目的开发语言了。何况现在还多了 Rust 这个选项。</p>
<p>偏偏碰上这么一个遗留项目。</p>
<p>只好把 C++ 临时捡起来,一顿重构。</p>
<p>原作者大概水平不高,或者写得不用心,又或者两者都有。代码质量很差,大量面条代码,动辄几百行的函数,宁可拷贝也不肯抽取函数,前后打脸的变量命名和代码逻辑,想当然的并发代码 …… 反正就一个编程规范的反面教材,吐槽不完。他大概也发现自己维护不下去,在我接手之前就撂挑子了。</p>
<p>重构过程按下不表,反正是一边猜测代码的意图,一边看运行效果去验证,等理解了,再去修正逻辑错误。</p>
<p>慢慢算是改出一点效果。但是还是有很多地方不太确定意图,为了保持跟服务端的兼容性,需要进行灰度更新。</p>
<p>出问题时,我需要能区分具体版本。</p>
<p>于是就有了上面的一出。</p>
<h3 id="补充"><a href="#补充" class="headerlink" title="补充"></a>补充</h3><p>关于 hash 部分,之所以说预测 hash 值只是 <strong>几乎</strong> 不可能,是因为还有一种办法:随机碰撞。</p>
<p>随机构造一个合法的 hash,尝试写入之后求真正的 hash,不断重试,直到构造的 hash 刚好和生成的 hash 一致为止。</p>
<p>理论上这是可以做到的。但这种做法会消耗大量的算力和时间。比特币挖矿的 POW 跟这个接近,但只是限定得到的 hash 的开头的零的数量,就已经耗费如此巨大的算力。如果要求算出的 hash 跟 构造的值 完全一致,需要的碰撞次数可能是天文数字。我不是这方面的专业人员,估算得可能不准确。但要说对普通开发电脑来说不可能,应该还是中肯的。(Google 破解 SHA-1 时,动用了大量的计算机集群,才构造出一个 PDF,获得预先构造的 hash)</p>
<p>而这,只是一次提交。难道每次提交都要动用超算,还要跑半天?</p>
<hr>
<p><img src="https://i.creativecommons.org/l/by-nc-sa/4.0/88x31.png" alt="知识共享 “署名-非商业性使用-相同方式共享” 4.0 (CC BY-NC-SA 4.0)”许可协议"><br>本文为本人原创,采用<a href="http://creativecommons.org/licenses/by-nc-sa/4.0/" target="_blank" rel="external">知识共享 “署名-非商业性使用-相同方式共享” 4.0 (CC BY-NC-SA 4.0)”许可协议</a>进行许可。<br>本作品可自由复制、传播及基于本作品进行演绎创作。如有以上需要,请留言告知,在文章开头明显位置加上署名(Jayce Chant)、原链接及许可协议信息,并明确指出修改(如有),不得用于商业用途。谢谢合作。<br>请点击查看<a href="http://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh" target="_blank" rel="external">协议</a>的中文摘要。</p>
]]></content>
<summary type="html">
<p>记在一个遗留 C++ 项目上改造版本号。</p>
</summary>
<category term="C++" scheme="https://jaycechant.info/tags/C/"/>
</entry>
<entry>
<title>告别 2021:近况与选题</title>
<link href="https://jaycechant.info/2022/farewell-2021/"/>
<id>https://jaycechant.info/2022/farewell-2021/</id>
<published>2022-01-01T12:57:54.000Z</published>
<updated>2022-01-18T11:22:10.207Z</updated>
<content type="html"><![CDATA[<p>看一下公众号,一不小心,半年没有推送过了。</p>
<a id="more"></a>
<p>眼看 2021 年就要过去了,跟大伙聊聊。</p>
<p>(是不是觉得这句话怪怪的。是的,太忙了,就这点字儿也是断断续续写,跨年了才发出来。)</p>
<h2 id="我去哪了?"><a href="#我去哪了?" class="headerlink" title="我去哪了?"></a>我去哪了?</h2><p>一句话总结:装修、陪考、带娃、搬家、适应新岗位。</p>
<p>你要是问,事情就真的安排得那么满,每天抽点时间写两个字都抽不出来?那倒不至于。</p>
<p>那些励志故事里,主人公同时身兼数职,还各种见缝插针阅读、学习、写作。跟他们相比,我的时间海绵真是水得可以,根本就没有用力挤。</p>
<p>但关键不在时间,而在精力。这半年多里,我一直觉得自己在玩一个大型的华容道。赤壁的火马上烧到眉毛,前面还有一堆大块头挡道,要小心翼翼腾挪,曲线救国。一个人考虑这些,哪怕没在干活,心里也一直在琢磨,精力处于亏空状态。</p>
<p>之前的文章说过,年岁渐长,我越是理解并接受,自己是个普通人,能力和精力有限。我与自己和解,不再幻想通过自我施压实现目标。不把自己逼疯,持续输出比较重要。</p>
<h2 id="华容道"><a href="#华容道" class="headerlink" title="华容道"></a>华容道</h2><p>新房很早就装修好了,剩一些细节收尾,通风散味。出于健康方面的考虑,原计划并不着急搬家。</p>
<p>直到租的房子,各种设施突然开始集中坏掉,像在催促我们走。</p>
<p>房东很好说话,就是太忙,疏于打理房子。这些年坏的东西,都是跟房东商量好,自己找人修。</p>
<p>但这回老化坏掉的设施,好几处修起来都比较麻烦。算一下折腾完也该搬走了。与其折腾修,不如凑合住着,赶紧收拾新房。</p>
<h3 id="搬家"><a href="#搬家" class="headerlink" title="搬家"></a>搬家</h3><p>在那个时间点,队友还在为换工作学习和考证,我妈帮忙带着孩子,我预定的工作还没开始。</p>
<p>干活的只能是我。而且只有我。</p>
<p>一个人吭哧吭哧在新家收尾和搞清洁,赶在队友去新单位上班前,把一家人挪过来。再回到租的房子,一个人吭哧吭哧地收拾旧东西。中间还要给不敢开车的队友当司机,送她去各个考场。有时队友闭关复习,还要一个人带娃。</p>
<p>别问我为什么不雇人。最后退房前的清洁就雇的保洁,快准狠。但更多的活,是自己才能做的细致活:</p>
<p>譬如说,新房有个墙面没找平,装好定制柜子才发现有很大空隙,想填腻子重新刷漆;找了很多师傅,活太小没人愿意接。又譬如,租的房子为了给娃腾空间,储物那个乱,偏偏蟑螂还很多;要是交给搬家公司直接搬,会夹杂垃圾和蟑螂一起带过来;想直接扔,里面又有很多重要的东西。诸如此类让人为难的活,只好自己干。</p>
<p>这样的活,一个接一个,直干到人崩溃。</p>
<p>但最后,还是庆幸选择亲力亲为,翻出了重要证件和回忆,过滤了数量庞大的垃圾和蟑螂。最后保洁的干活速度之快,来不及交代的东西都成了垃圾——幸好已经没有重要的东西了。</p>
<h3 id="新岗位"><a href="#新岗位" class="headerlink" title="新岗位"></a>新岗位</h3><p>晚上搬到新家,第二天早上队友就去了十几公里外的单位上班。</p>
<p>这个距离,按闲时半小时车程,多数人会选择开车。可队友刚到新岗位,需要时间熟悉业务。高峰动辄50分钟起步通勤时间,加上她生疏的车技,让她决定先住在单位宿舍。</p>
<p>我这边,原定的项目,因为疫情和政策没了下文。等忙完搬家,正准备寻找机会的时候,两位老朋友联系到我。</p>
<p>简单说,业务上升期,技术严重缺人,承诺很高的技术权限和较大的时间自由。</p>
<p>看到技术上有可作为的地方,也考虑到队友的情况需要我兼顾家庭,稍微考虑之后我就加入了。</p>
<p>接下来是另外一个大型华容道。</p>
<h3 id="多线程"><a href="#多线程" class="headerlink" title="多线程"></a>多线程</h3><p>业务快速上升和技术缺人,带来的是大量的技术债,到处是坑,狼烟四起。</p>
<ul>
<li>长远来看,要重新规划技术路线,一点点梳理和重构基础设施。</li>
<li>中短期内,要维持目前的业务运转,甚至跟上新增业务的需求。</li>
<li>为了达到以上两个目的,显然需要更多的人,招聘。</li>
</ul>
<p>而招聘、还有管理,这些跟人打交道的事情,比想象中更消耗精力。</p>
<p>我内向且慢热,上下文切换开销大,却被多个线程撕扯着。招聘准备不足,忙活了一星期,没有合适的,赶紧先停掉;重写基础组件,写到一半,被别的事情拽走。最终还是救火占了上风,各种琐碎的事都得亲力亲为。毕竟要是大本营都烧没了,其它事情无从谈起。</p>
<p>我并不介意干琐碎的技术活,甚至乐意保持对技术细节的掌控感。可后面还有一堆事情等着处理时,一个人在不同技术角色来回切换,硬着头皮干一些生疏的活,这种低效率其实是一种奢侈的浪费,却没有别的办法。</p>
<p>人少、身兼多职的低效、对遗留代码不够熟悉,加上一个晚上不肯睡早上不肯起的娃,尽管除了睡觉和带娃的时间都扑在工作上,各种日程还是常常 delay。</p>
<p>连续工作,还有连续 delay 带来的压力,在慢慢侵蚀我的状态乃至自信心。</p>
<p>多亏有其他同事一起坚持,才看到一点摆脱这种状态的希望。</p>
<h3 id="喜欢上开车"><a href="#喜欢上开车" class="headerlink" title="喜欢上开车"></a>喜欢上开车</h3><p>长期处于这种状态下,突然发现自己喜欢上开车。</p>
<p>我本来不喜欢开车。汽车只是移动工具,没有必要非得自己掌控。本可以利用移动时间处理事情,或者干脆补觉,一旦坐在主驾位置上,自己倒成了车子的人肉 CPU,不仅不能干自己的事,还得全程把注意力搭进去。如果碰上堵车或者长途,就更累人了。</p>
<p>这样看当司机实在很亏。我总是乐意优先选择公共交通,或者坐在副驾。虽然偶尔也会有开车兜风的冲动,但是对于当义务司机一向都不积极。所以总是催促家里的本上老司机,重新把开车练熟,我好坐副驾睡觉。</p>
<p>最近我突然积极起来。</p>
<p>一次送队友去单位的路上,我突然明白过来:开车的过程,以『交通安全』的名义,把我从日常的思虑和压力中,拉了出来。</p>
<p>交通安全,或者说生命安全,有着天然的道德优势。在这段时间里,我可以暂时不去想系统的架构,不考虑开发任务的优先级,不接听救火或者开会的电话,而不必有心理上的负担。与之相比,已经熟练到可以下意识进行的驾驶行为,反而是比较舒服的。</p>
<h2 id="致歉"><a href="#致歉" class="headerlink" title="致歉"></a>致歉</h2><p>这样的日子里,一个晚上电力充沛的娃,把睡前学习码字的路也堵了。</p>
<p>等把他哄睡,再起来收拾一下,就已经太晚了。</p>
<p>这还算是好的,赶上娃感冒咳嗽,晚上基本就不可能睡踏实了。也有些时候,白天太累,娃还没睡着我先打瞌睡,等娃睡着,起来收拾完,反而精神了。这时知道明天是不可能早起了,干脆起来把明天上午的任务先处理一些。</p>
<p>日程压力在那,大块时间优先干活,小块时间优先休息。除此以外的事情,都有不务正业的愧疚感——哪怕是跟业务结合,提前给同事写技术教程。毕竟一时还用不到。</p>
<p><br></p>
<p>不怕大家笑话,太久不更新,我甚至有点害怕更新,害怕大家发现还关注了这个鸽子公众号,然后顺手取关。所以心里攒着劲,复更要更点有用的东西,好留住大家。</p>
<p>没有想到半年里,因为大佬们的转载,读者数居然还涨了一点。</p>
<p>没有更新,自然想不起来去看后台,因此错过了一些留言。</p>
<p>错过了土拨鼠大佬(公众号:Go招聘)的转载请求。错过了网友的加好友请求。</p>
<p>这里向被我放鸽子的读者,向留言但没有得到回复的朋友,一并说一声抱歉。</p>
<p>(微信为了避免公众号骚扰读者,只能读者主动找公众号。留言必须 48 小时内回复。等我看到留言的时候,早过了时限,不是我耍大牌。)</p>
<p>我不是大佬,如果大家不嫌弃,可以加微信『MrArchive』一起交流。加好友请写明来意,类似『公众号读者,Go 语言交流』这样。不然卖课程、卖房子、卖各种广告的人太多了。</p>
<h2 id="告别-2021"><a href="#告别-2021" class="headerlink" title="告别 2021"></a>告别 2021</h2><p>这就是我忙碌又混乱的 2021 (下半年)。</p>
<p>回头想想,混乱从 19 年就开始了。彼时我们全家都在期待新生命的到来,阿公阿婆(以及另外几位亲人)都还健在,大家还不知道什么是新冠。</p>
<p>接下来的生活逐渐失控,保胎、孩子出生、房子维修和装修、工作调整 …… 中间伴随着影响全球的疫情,以及好几场送别。不过跟全球经济寒冬相比,跟某些行业、跟感染者和医护人员比,我们受到的影响算是很小的了。</p>
<p>或者说,其实在我几年前从外企离职时,在买下这套房子时,在我们决定要一个孩子时,就注定了路不会太好走。</p>
<p>这些年下来,我的感受是,混乱并不可怕,没有希望才可怕。</p>
<p>毕竟把时间和空间的尺度拉大,混沌才是常态,秩序则更像是偶然。</p>
<p>未来的日子,可能适应混乱,拥抱变化,才是唯一的出路。</p>
<p>虽然这对一个强迫症来说并不容易。</p>
<h2 id="更新"><a href="#更新" class="headerlink" title="更新"></a>更新</h2><p>那我还更新吗?</p>
<p>更。</p>
<p>一定会更。</p>
<p>只会因为忙而鸽,但绝不会停。哪怕将来有一天微信公众号这个平台下线了,也会找到另外的地方继续更新。</p>
<p>这首先是为了自己记录。</p>
<p>忘了在哪里看过一句话:没有纳入版本控制(VCS)的代码,就跟没有写过一样(大意)。</p>
<p>同理,所有没有记录下来的灵光一闪,最终都会遗忘在时间长河里,不再属于你。只有成为文字,才是你的。</p>
<p>这个道理过去已经验证过很多次。解决过的问题,过几年遇到依旧生疏,甚至看到自己留下的文档的第一感觉:如看天书。</p>
<p>人菜,还不存档,怎么通关。何况人生这个游戏,通关之路漫长。</p>
<h3 id="人和事"><a href="#人和事" class="headerlink" title="人和事"></a>人和事</h3><p>而且这几年,还发现,有别的需要记录:人和事。</p>
<p>如果说,之前是一个初出社会的年轻人,试图积累手里的技,和记录心里的悟。</p>
<p>那么现在,过了而立之年的人,还想尝试留住一些过往。</p>
<p>小时候觉得理所当然一直都会在的人,一一与我道别。</p>
<p>不,更多时候根本来不及告别。</p>
<p>《寻梦环游记》说,人会死去三次,最后一次是被世人遗忘之时。作为一个受过高中物理训练的人,觉得这种说法过于文艺——无论你是否记得,他们本人都不再有任何感知。</p>
<p><a href="http://zhishifenzi.com/depth/depth/7233.html" target="_blank" rel="external">你我皆是星辰之子</a>,从星尘中来,终究回到星尘中去。</p>
<p>饶是明白这些道理,我仍愿意记得他们。那些音容笑貌,今天在脑海里依然清晰,梦中还偶尔遇见,但不知道哪天就再也想不起。</p>
<p>我愿意借助互联网,让他们存在过的痕迹,稍微多保留一会,甚至超过我存在的时间。(愿公众号,或者未来可能换的托管服务,基业长青)</p>
<p>不去后悔过去与他们相处太少,因为总的时间有限,生命再来一次,不见得会分配得更好,无非蝴蝶效应、厚此薄彼。记住,是能做的最后一件事。</p>
<h3 id="读者"><a href="#读者" class="headerlink" title="读者"></a>读者</h3><p>不过,也不光为自己记录。我也需要读者。</p>
<p>不然写私密日记好了。</p>
<p>我需要来自读者的监督和鼓励,需要交流和指正。</p>
<p>那些纪念,也希望有更多人知道,他们来过,一些事发生过。</p>
<p>如果碰巧博得些名气,顺便,换点钱,或者给工作带来助力,更好。想要名利,改善生活,不寒碜。</p>
<p>可我的读者,隔着网络,面目模糊。</p>
<p>这是大多数内容输出者早期都会遇到的问题。</p>
<p>在跨过某个临界点之前,会有三个问题会被反复提起:</p>
<ul>
<li>想输出什么?</li>
<li>擅长什么?</li>
<li>受众是什么人,喜欢什么?</li>
</ul>
<p>然后为了找到答案,在不同尝试之间,反复横跳。</p>
<p>随便找一个已经实现稳定输出、有固定受众的博主(包括做视频的),翻到最早期的作品,大概率可以看到内容和风格在不断调整。</p>
<p>直到,在那三个问题的答案里,找到交集。</p>
<p>又或者,在不可能三角里,放弃掉一端或两端。</p>
<p>全职博主要吃饭,受众优先,个人志趣做一定让步,技能树不妨现点。</p>
<p>个人表达优先,就要有缺乏受众的心里准备,耐得了寂寞。</p>
<p>大 V 也有过摸索的阶段。</p>
<h3 id="选题和深度"><a href="#选题和深度" class="headerlink" title="选题和深度"></a>选题和深度</h3><p>读者面目模糊的直接结果,是选题和深度都拿不准。</p>
<p><br></p>
<p>一直想写的,像 Go 语言 和 粤语 的内容,一定会写下去。区别在于写多细、多深。</p>
<p>因为不想糊弄,目标往往定得略超出自己的能力范围——费曼技巧,借输出以输入——写完自己也得到提升。</p>
<p>这样输出速度就慢。空闲时间不多时,自然更慢、甚至暂停。</p>
<p>而这样艰难的输出,反响平平。找身边的朋友问,“感觉有点硬,先收藏,以后慢慢看” 。</p>
<p>不见得内容真有多深奥,只是个人储备有限,勉强深入之后,就没有余裕浅出了。</p>
<p><br></p>
<p>主食硬菜毕竟有限,之间的空档,需要开胃小菜。一边跟读者保持联系,另一边保持更新习惯和码字手感。</p>
<p>上面是个人爱好,这部分总得讨好一下受众。</p>
<p>但我就是不会选题。</p>
<p>本来码字时间就难得。</p>
<p>本来写东西就拧巴,跟自己较劲。</p>
<p>好不容易写出来的东西,大家没兴趣。</p>
<p>怎能不打击积极性。</p>
<p>也想过蹭热点,但不想当标题党,要借热点展开聊点啥。结果一深入写,热点凉了。</p>
<h3 id="身边人"><a href="#身边人" class="headerlink" title="身边人"></a>身边人</h3><p>改善大概没有捷径。</p>
<p>无非,内容质量,输出技巧,贴近受众。</p>
<p>废话,这恰恰最难,大家都在上面使劲。</p>
<p>虽然慢,知识水平还是在努力提升的(特别是工作相关内容);表达也有反复修改,刻意练习。还好暂时不靠输出吃饭,心态不至于失衡。</p>
<p>一直没进步的,就剩下与读者的交流。不知道你们是谁,听不见你们的声音(难得的回复还让我错过了),不知道哪些内容对你们有用,不知道哪些地方写得不好……</p>
<p>既然努力也没用,干脆暂时『放弃』读者交流。</p>
<p>大家关注,更多只是偶然,愿意试试看内容是不是对口。我自己都尚未形成风格和节奏,没办法给大家稳定的预期,又怎能期待大家给我清晰的反馈。</p>
<p>考虑到关注者里有现实中的亲友,关注是支持,并不一定内容对口,谈不上给意见;考虑到很多陌生人更愿意安静地看,没有动力发表意见——不合适大不了不看(我作为读者时何尝不是)。</p>
<p>以目前的关注数,没有声音、面目模糊再正常不过。</p>
<p>我只看到了别人的冰山露了尖,却不知道别人水下看不见的冰山有多大。现在想这些,纯属庸人自扰。</p>
<p><br></p>
<p>其实就是太贪心。试图一下子从『为自己写』,跳到『为广大读者写』。结果不知道读者是谁,也忘了自己是谁。</p>
<p>明明连身边的人的需求,都还没服务好。那些身边人提的问题,都还没好好回答。</p>
<p>那就从『为自己写』,先往外扩一小圈,变成『为身边人写』。</p>
<p>这样一来,读者就一下子有了名字和面孔,如何确定选题和深度也一下子清晰起来。</p>
<p>卷子答得好不好,总归可以问问出题人吧。</p>
<p>我身边有哪些读者呢。</p>
<ul>
<li><p>家人:</p>
<ul>
<li>队友用电脑,偶有这样那样的问题,给她解答过的,可以有 #给普通人的 IT 科普# 。(名字有点啰嗦,有没有更言简意赅的建议?)</li>
<li>为一天天长大的儿子准备,可以有 #有趣的粤语# 。小孩子长得很快,一眨眼就过去了,得先攒点存货。</li>
</ul>
</li>
<li><p>技术同事:</p>
<ul>
<li>新同事入职肯定要同步一下环境搭建、工具选用、开发规范。不涉密的部分,不妨共享出来给大家提提意见。</li>
<li>不用说,肯定有开发的内容。但选题就可以更贴近工作的实际,避免自己和同事们重复踩坑。考虑技术栈用 Go,相关的教程当然继续更。</li>
</ul>
</li>
<li><p>非技术同事和客户:</p>
<p> “这技术好像很厉害,但我听不懂。” 跟非技术人员打交道,沟通不到位,容易对技术方案的效果、实现难度、工期 等很多细节理解有偏差。那么对于工作中高频出现的、比较关键的内容,也有写科普的必要。</p>
</li>
<li><p>然后是一群信任我的朋友们,遇事不决会问我意见。那些我能回答、或者感兴趣愿意一起研究的,思考讨论的过程也可以分享出来,姑且叫 #万能的朋友圈# 。</p>
</li>
</ul>
<h2 id="顺便,打个广告?"><a href="#顺便,打个广告?" class="headerlink" title="顺便,打个广告?"></a>顺便,打个广告?</h2><p>能坚持看唠叨看到这里的,想必是忠实读者了。</p>
<p>前面说了,我们现在很缺人。所以我想,有没有可能,正好有年轻读者也在寻找工作机会呢。</p>
<p>我们做无人零售设备和系统,研发团队 base 广州黄埔。主要是自己的业务,但也有客户定制化的开发。</p>
<p>软件技术栈方面</p>
<ul>
<li>后端现阶段主要是 Java(SpringBoot),有用到一些现成的 PHP 项目,计划用 Go 重写一部分基础组件;</li>
<li>前端 Web 和 小程序 都有,框架主要用 Vue;</li>
<li>设备端用 Go(Linux)和 Android。</li>
</ul>
<p>各个岗位(Java / Go / PHP / 前端 / Android)都需要人,有相关语言经验的朋友,可以加微信私聊。考虑到现阶段有经验的 Go 开发者不多,而且主要被大厂垄断,我们对直接招不抱太大希望,可以你先来,我们教。(至少需要一门其它语言的开发经验,和基本功过关。)</p>
<p>虽然上面抱怨了那么多,而且小公司的薪资也比不上大厂的诱人。但选对了,小公司也有好处。首先是技术的参与度高,成长快;万一公司真的做大做强了,能享受到早期员工的红利;然后是人际关系更简单,没那么多办公室政治。</p>
<p>当然,不是所有小公司都这样,也有很多公司,人没几个,就管理混乱,毛病一堆。大公司也有大公司病,不是所有岗位都重要,一些边缘岗位,特别消磨人的热情。还是要具体接触了解。</p>
<p>我就说两点:我们不缺业务量和客制化订单,反而因为人手不足,不得不拒绝一些订单;我们的人员整体都非常年轻,氛围很好,内部互相都喊名字——不仅不带职位敬称,连『哥』、『姐』也免掉,直接喊名字。</p>
<p>合适的人不好招,估计未来一段时间内都会缺人,暂时不方便离岗的,也可以先聊着。不在公众号展开太多,以免打扰到不需要的读者。</p>
<hr>
<p><img src="https://i.creativecommons.org/l/by-nc-nd/4.0/88x31.png" alt="知识共享 “署名-非商业性使用-禁止演绎” 4.0 (CC BY-NC-ND 4.0)”许可协议"><br>本文为本人原创,采用<a href="http://creativecommons.org/licenses/by-nc-nd/4.0/" target="_blank" rel="external">知识共享 “署名-非商业性使用-禁止演绎” 4.0 (CC BY-NC-ND 4.0)”许可协议</a>进行许可。<br>本作品可自由复制、传播。如有以上需要,请留言告知,在文章开头明显位置加上署名(Jayce Chant)、原链接及许可协议信息,原文引用(不可发布基于本作品的二次创作),不得用于商业用途。谢谢合作。<br>请点击查看<a href="http://creativecommons.org/licenses/by-nc-nd/4.0/deed.zh" target="_blank" rel="external">协议</a>的中文摘要。</p>
]]></content>
<summary type="html">
<p>看一下公众号,一不小心,半年没有推送过了。</p>
</summary>
<category term="blog" scheme="https://jaycechant.info/tags/blog/"/>
</entry>
<entry>
<title>Go 语言实战(9):指针、引用和值</title>
<link href="https://jaycechant.info/2021/golang-in-action-day-9-pointer-reference-and-value/"/>
<id>https://jaycechant.info/2021/golang-in-action-day-9-pointer-reference-and-value/</id>
<published>2021-05-16T07:51:22.000Z</published>
<updated>2021-05-16T15:48:40.571Z</updated>
<content type="html"><![CDATA[<p>在经过编写 CLI 程序的尝试之后,我们继续回来聊 Go 语言的基础知识。</p>
<p>相信实际写过一些代码之后,会更容易理解。</p>
<a id="more"></a>
<p>原计划这期聊 数组和切片。考虑到聊切片时,无论如何绕不开指针和引用的话题,干脆提到前面来。</p>
<h2 id="目录"><a href="#目录" class="headerlink" title="目录"></a>目录</h2><p>[TOC]</p>
<h2 id="指针"><a href="#指针" class="headerlink" title="指针"></a>指针</h2><p>指针(Pointer)本质上是一个<strong>指向</strong>某块计算机内存的地址。就像日常的门牌地址一样。只不过内存地址是一个数字编号,对应的是一个个字节(byte)。</p>
<p>当然,高级语言能访问到的内存,经过了操作系统内存管理的抽象,并不是连续的物理内存,而是映射得到的虚拟内存。但现在不必关注这些细节,当它是连续内存就好。</p>
<p><img src="../../images/golang-in-action-day9/pointer-mem.svg" alt=""></p>
<p>出于<strong>内存安全</strong>和<strong>屏蔽底层细节</strong>的考虑,C++ 以后的高级语言大多不再支持指针,而是改为使用『引用』。引用和指针的差别,我们后面说。</p>
<p>Go 作为 C 的『嫡亲』后继,为了性能和灵活性,保留了指针,而且用法基本一样。但 Go 增加了 <strong>逃逸分析</strong> 和 <strong>垃圾回收(GC)</strong>,一定程度上解决掉了 悬挂指针 和 内存泄漏 的问题,降低了开发者的认知负担。(注意,Go 还是可能发生内存泄漏,只是需要特定的条件,发生概率大大降低了。)</p>
<h3 id="Go-指针"><a href="#Go-指针" class="headerlink" title="Go 指针"></a>Go 指针</h3><p>先上代码,来点直观认识</p>
<figure class="highlight go"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div></pre></td><td class="code"><pre><div class="line"><span class="comment">// 声明</span></div><div class="line"><span class="keyword">var</span> pa, pb *<span class="keyword">int</span></div><div class="line"></div><div class="line"><span class="comment">// 取址</span></div><div class="line"><span class="keyword">var</span> a = <span class="number">10</span> <span class="comment">// 默认为 int</span></div><div class="line">pa = &a</div><div class="line"></div><div class="line"><span class="comment">// 解引用(取值)</span></div><div class="line"><span class="keyword">var</span> b = *pa</div><div class="line"></div><div class="line"><span class="comment">// 输出</span></div><div class="line">fmt.Println(<span class="string">"a:"</span>, a)</div><div class="line">fmt.Println(<span class="string">"b:"</span>, b)</div><div class="line">fmt.Println(<span class="string">"&a:"</span>, &a)</div><div class="line">fmt.Println(<span class="string">"&b:"</span>, &b)</div><div class="line">fmt.Println(<span class="string">"pa:"</span>, pa)</div><div class="line">fmt.Println(<span class="string">"pb:"</span>, pb)</div><div class="line">fmt.Println(<span class="string">"*pa:"</span>, *pa)</div><div class="line">fmt.Println(<span class="string">"*pb:"</span>, *pb)</div></pre></td></tr></table></figure>
<blockquote>
<p>关于取址运算符 <code>&</code> 和 解引用运算符 <code>*</code> 的详细介绍(优先级、可寻址等内容),请参考第 4 期的《<a href="../../2020/golang-in-action-day-4/">运算符</a>》。</p>
<p>解引用 dereference:取址 address 的反操作,意味根据类型,从地址中取出对应的值。</p>
</blockquote>
<p>上面的代码输出</p>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div></pre></td><td class="code"><pre><div class="line">a: 10</div><div class="line">b: 10</div><div class="line">&a: 0xc0000140a0</div><div class="line">&b: 0xc0000140a8</div><div class="line">pa: 0xc0000140a0</div><div class="line">pb: <nil></div><div class="line">*pa: 10</div><div class="line">panic: runtime error: invalid memory address or nil pointer dereference</div></pre></td></tr></table></figure>
<p>指针的零值是 <code>nil</code> ,对一个 <code>nil</code> 指针解引用会引起运行时错误,引发一个 panic。</p>
<p>通过下图,可以清晰看到4 个变量之间的关系。</p>
<p><img src="../../images/golang-in-action-day9/example1.svg" alt=""></p>
<blockquote>
<p>注1:<code>int</code> 类型在 64 位机器上是 64 位,占据 8 个字节。</p>
<p>注2:两个指针实际上也是保存在内存上,但是为了特意区分,也为了避免内存的图示画得太长,所以把它们单独放在左边示意。</p>
</blockquote>
<p>指针允许程序以简洁的方式引用另一个(较大的)值而不必拷贝它,允许在不同的地方之间共享一个值,可以简化很多数据结构的实现。保留指针,让 Go 的代码更灵活,以及更好的性能表现。</p>
<h3 id="指针的类型"><a href="#指针的类型" class="headerlink" title="指针的类型"></a>指针的类型</h3><p>指针是派生类型,派生自其它类型。类型 <code>*Type</code> 表示『指向 Type 类型变量的指针』,常常简称『Type 类型的指针』,其中 <code>Type</code> 可以为任意类型,被称作指针的 基类型(base type)。换言之,从 <code>Type</code> 类型,派生出 <code>*Type</code> 类型。</p>
<p>前面说到,内存地址是一个编号,指针的底层类型(underlying type)相当于是整型数(<code>uintptr</code>),宽度与平台相关,保证可以存下内存地址。</p>
<p>但指针又不仅仅是一个整型数,上面还附加了类型信息。指针指向的类型不同,派生出的指针类型也不同。所以指针不是一个类型,而是一类类型;类型有无数多种,对应的指针(包括指向指针的指针)的类型也有无数种。</p>
<p><code>*int16</code> 跟 <code>*int8</code> 就是不同类型。它们虽然存了同样长度的地址,但 基类型 不同,解引用时会有不同的行为。不同类型的指针之间无法进行转换。(除非通过 <code>unsafe</code> 包进行强制转换。包名 unsafe 道出风险,这个包里的都是危险操作,后果自负。)</p>
<figure class="highlight go"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div></pre></td><td class="code"><pre><div class="line"><span class="comment">// 为了方便理解,写成二进制,高8位的字节是 3,低 8 位的字节是 1,对应的数字是 3x2^8+1 = 769</span></div><div class="line"><span class="keyword">var</span> c <span class="keyword">uint16</span> = <span class="number">0</span>b00000011_00000001</div><div class="line">pc16 := &c</div><div class="line">fmt.Println(<span class="string">"pc16:"</span>, pc16)</div><div class="line">fmt.Println(<span class="string">"*pc16:"</span>, *pc16)</div><div class="line"><span class="comment">// 为了演示,将 *uint16 强制转换为 *uint8,实际开发中不推荐,除非你清楚自己在做什么</span></div><div class="line">pc8 := (*<span class="keyword">uint8</span>)(unsafe.Pointer(pc16))</div><div class="line">fmt.Println(<span class="string">"pc8:"</span>, pc8)</div><div class="line">fmt.Println(<span class="string">"*pc8:"</span>, *pc8)</div></pre></td></tr></table></figure>
<p>输出</p>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div></pre></td><td class="code"><pre><div class="line">pc16: 0xc0000a2058</div><div class="line">*pc16: 769</div><div class="line">pc8: 0xc0000a2058</div><div class="line">*pc8: 1</div></pre></td></tr></table></figure>
<p>可以看到,两个指针保存了同样的地址,按理说解引用取出的内容应该是一样的。但事实是,解引用还跟类型相关:地址只指明了取内容的起点,基类型指定取多少个字节,以及如何解释取出来的比特。在这里,对 <code>*uint16</code> 解引用取出了两个字节,按整型数解释为 <code>796</code> ;对 <code>*uint8</code> 解引用则取了一个字节,解释为 <code>1</code> 。</p>
<p><img src="../../images/golang-in-action-day9/pointer-base-type.svg" alt=""></p>
<p>这里还得知了一个额外的信息:我的电脑是小端字节序,换句话说,数字是从低字节到高字节存储的,也就是 <code>00000001 00000011</code> ,跟手写的习惯是相反的,所以才会在只取一个字节时,取到了低字节。</p>
<h3 id="逃逸分析与垃圾回收"><a href="#逃逸分析与垃圾回收" class="headerlink" title="逃逸分析与垃圾回收"></a>逃逸分析与垃圾回收</h3><p>在 C/C++ 里面使用指针,容易发生两类问题:</p>
<ul>
<li><p>悬空指针(dangling pointer):又叫野指针(wild pointer),是指非空的指针没能指向相应类型的有效对象,或者换句话说,不能解析到一个有效的值。这有可能是对指针做了错误的运算,或者目标内存被意外回收了。</p>
</li>
<li><p>内存泄漏(memory leak):是指因为疏忽或者错误,没有释放已经不再使用的内存,造成内存的浪费。在 C/C++ 这类没有内存管理的语言里,常见的泄漏原因是在释放动态分配的内存之前,就失去了对这些内存的控制。</p>
</li>
</ul>
<p>Go 里面不允许对指针做算术运算,基本排除对指针运算错误导致的问题。剩下还能出问题的,就是释放内存的时机:释放早了,悬空指针;释放晚了或者干脆没释放,内存泄漏。来看看 C 的例子:</p>
<figure class="highlight c"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div><div class="line">20</div><div class="line">21</div><div class="line">22</div><div class="line">23</div><div class="line">24</div><div class="line">25</div><div class="line">26</div><div class="line">27</div><div class="line">28</div></pre></td><td class="code"><pre><div class="line"><span class="comment">// 注意这是 C 代码,不要跟 Go 代码混淆</span></div><div class="line"><span class="meta">#<span class="meta-keyword">include</span> <span class="meta-string"><malloc.h></span></span></div><div class="line"></div><div class="line"><span class="function"><span class="keyword">int</span>* <span class="title">getPtrOnStack</span><span class="params">()</span></span></div><div class="line">{</div><div class="line"> <span class="comment">// n 分配在栈上,函数返回即被回收</span></div><div class="line"> <span class="keyword">int</span> n;</div><div class="line"> <span class="keyword">int</span>* pi = &n;</div><div class="line"> <span class="keyword">return</span> pi;</div><div class="line">} </div><div class="line"></div><div class="line"><span class="function"><span class="keyword">int</span>* <span class="title">mallocInt</span><span class="params">()</span></span></div><div class="line">{</div><div class="line"> <span class="comment">// 动态分配的内存分配在堆上,需要自行释放</span></div><div class="line"> <span class="keyword">return</span> (<span class="keyword">int</span>*)<span class="built_in">malloc</span>(<span class="keyword">sizeof</span>(<span class="keyword">int</span>));</div><div class="line">}</div><div class="line"></div><div class="line"><span class="function"><span class="keyword">int</span> <span class="title">main</span><span class="params">()</span></span></div><div class="line">{</div><div class="line"> <span class="comment">// pi 为悬空指针</span></div><div class="line"> <span class="keyword">int</span>* pi = getPtrOnStack();</div><div class="line"> <span class="keyword">int</span>* pi2 = mallocInt();</div><div class="line"> <span class="comment">// 申请的内存没有释放,应该先 free(pi2)</span></div><div class="line"> pi2 = <span class="number">0</span>;</div><div class="line"> <span class="comment">// pi2 置零后,失去了对未释放内存的控制,因为地址已经找不回了</span></div><div class="line"> <span class="comment">// 短时间内一次过运行的程序内存泄漏问题不大,到程序退出都会释放;</span></div><div class="line"> <span class="comment">// 但对于需要持续运行的程序,内存泄漏会造成严重后果。</span></div><div class="line">}</div></pre></td></tr></table></figure>
<p>Go 的解决方案是</p>
<ul>
<li><p>逃逸分析:由编译器对变量进行逃逸分析,判断变量的作用域是否超出函数的作用域,以此决定将内存分配在栈上还是堆上,不需要人工指定。这就解决了第一个问题,函数内部声明的变量,其内存可以在函数返回后继续使用。</p>
</li>
<li><p>垃圾回收:由运行时(runtime)负责不再引用的内存的回收。回收算法一直在改进,这里不展开。这就解决了第二个问题,当内存不再使用的时候,只要不引用即可(指针置零,或者指向别的内存),不需要手动释放。</p>
</li>
</ul>
<p>因为这些改进,Go 里面的指针看起来跟 C/C++ 差不多,实际使用的负担却小很多。</p>
<p>需要注意的是,垃圾回收无法解决『逻辑上』的内存泄漏。这是指程序逻辑已经不再用到某些内存,但是仍然持有这些内存的引用,导致垃圾回收无法识别并回收这些内存。这就好比清洁工只能保证地上和垃圾桶的干净,却无法判断办公桌上有哪些东西是没用的。</p>
<h3 id="字段选择器"><a href="#字段选择器" class="headerlink" title="字段选择器"></a>字段选择器</h3><p>对于操作数 <code>x</code> ,如果想访问它的成员字段或者方法,可以使用字段选择器(field selector),实际上就是一个句点 <code>.</code> 加上字段名。</p>
<p>举例说 <code>p</code> 是 <code>Person</code> 类型的变量,而 <code>Person</code> 有一个 <code>Name</code> 字段和 <code>Run()</code> 方法,就可以通过 <code>p.Name</code> 和 <code>p.Run()</code> 访问。</p>
<p>这部分的详细内容,要等到结构体和方法部分再展开。这里只提一点与 C/C++ 的区别。</p>
<p>还是以 <code>p</code> 和 <code>Person</code> 为例。在 C/C++ 里,只有 <code>p</code> 是一个 <code>Person</code> 类型变量的时候(相当于Go 语言的 <code>var p Person</code> ),才能用句点访问成员字段。如果 <code>p</code> 是一个 <code>Person</code> 类型的指针(相当于 Go 的 <code>var p *Person</code> ),则要用箭头操作符 <code>-></code> 访问成员。<code>p->Name</code> 跟 <code>(*p).Name</code> 等价。</p>
<p>Go 里没有箭头操作符。两种操作都用字段选择器 <code>.</code> 表示。实际上这是 Go 提供的一个语法糖,当Go 发现 <code>p</code> 是一个指针而且没有相应名字的成员时,会自动在 <code>*p</code> 里寻找对应的成员。</p>
<p>这样做,好处是省了一个操作符(Go 真的很省操作符和关键字),并且将值变量和指针变量的使用统一起来,在很多场景中可以不必关心使用的是一个值还是一个指针。而坏处也在于,在一些场景混淆了这两者。这个也是到结构体和方法时再细说。这里给一个直观的例子:</p>
<figure class="highlight go"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div></pre></td><td class="code"><pre><div class="line"><span class="keyword">type</span> Person <span class="keyword">struct</span> {</div><div class="line"> Name <span class="keyword">string</span></div><div class="line">}</div><div class="line"></div><div class="line"><span class="keyword">var</span> d Person</div><div class="line">d.Name = <span class="string">"David"</span></div><div class="line">fmt.Println(<span class="string">"d.Name"</span>, d.Name) <span class="comment">// 输出 d.Name David</span></div><div class="line"></div><div class="line">pd := &d</div><div class="line">fmt.Println(<span class="string">"pd.Name"</span>, pd.Name) <span class="comment">// 输出 pd.Name David</span></div><div class="line"></div><div class="line"><span class="comment">// 这部分无法通过编译,错误是 ppd.Name undefined (type **Person has no field or method Name)</span></div><div class="line"><span class="comment">// ppd := &pd</span></div><div class="line"><span class="comment">// fmt.Println("ppd.Name", ppd.Name)</span></div></pre></td></tr></table></figure>
<p>从 <code>**Person</code> 的角度看,会觉得很不讲理:明明 <code>*Person</code> 也没有 <code>Name</code> 这个字段啊,为什么 <code>pd</code> 不报错?</p>
<p>因为编译器识别到它是一个指针,自动从 <code>*pd</code> 里找字段。但是这个忙只帮忙向下找一层,对于 <code>ppd</code> ,<code>ppd.Name</code> 不存在,<code>(*ppd).Name</code> 也没有,就放弃了。</p>
<p>不像在 C/C++ 里很多操作都依赖指针,指针的指针并不少见,Go 里很少用到多级指针,所以这种语法糖只包一层大部分情况够用。</p>
<h2 id="指针、引用和值"><a href="#指针、引用和值" class="headerlink" title="指针、引用和值"></a>指针、引用和值</h2><blockquote>
<p>这三个概念既存在包含关系,又存在对比,解释起来非常拗口。如果你看完之后还是云里雾里,请耐心再多看几遍,或者实际写代码感受一下。如果还是不能理解,一定是我水平的问题,请先跳过这一部分。欢迎留言告知你的想法。</p>
</blockquote>
<p>在第 2 期《<a href="../../2020/golang-in-action-day-2/">常量与变量</a>》里,有提到值的定义:『无法进一步求值的表达式(expression)』,例如 <code>4 + 3 * 2 / 1</code> 的<strong>值</strong>是 <code>10</code> 。而常量和变量,则可以理解为值的容器。(尽管常量在具体实现上,往往是编译期直接替换为目标值。)</p>
<p>这个定义,强调与量并列。</p>
<p>值也可以理解为『可以被程序操作的实体的表示』。这时不强调与量的区别,如果一个变量保存了一个值,出于方便,有时也称这个变量为一个值。</p>
<p>虽然标题将指针、引用和值并列,其实<strong>引用和指针,本身也是值</strong>。它们都用来表示『可被程序操作的实体』。</p>
<p>同时<strong>指针是引用的一种</strong>,是最简单的透明引用。</p>
<p>换言之,三者之间构成这样一种包含关系:引用是值的一种特例,是一类<strong>可以间接访问其它值的值</strong>,区别于直接使用的值;指针是引用的一种特例,是一类简单的<strong>透明引用</strong>,区别于不透明的引用。</p>
<h3 id="指针和值"><a href="#指针和值" class="headerlink" title="指针和值"></a>指针和值</h3><p>先对比指针和值。</p>
<p>如果不考虑实际使用,从理论上说,指针类型跟别的整型一样,也是一个『可操作实体』,所以它也是值。在Go 里,指针跟所有值一样,赋值和参数传递的时候发生了拷贝。</p>
<p><img src="../../images/golang-in-action-day9/copy-on-assignment.svg" alt=""></p>
<p>但在使用中,大部分情况下,指针只是改善性能(避免拷贝)、提高代码灵活性(共享对象)、实现复杂数据结构的工具。我们<strong>并不关心指针的值本身,而是关心指针指向的值</strong>。为了方便讨论,指针变量跟它指向的值,常常会被等同看待。就像送礼或者颁奖时,不会有人举着汽车交给对方,而是会递交车钥匙;我们会将拿到车钥匙等同于拿到了车。(特别是 Go 取消了箭头操作符 <code>-></code>,值和指针都用同样的方式访问成员,更是弱化了这个区分。)</p>
<p>几乎没有人会关心指针保存的地址值是多少,只会关心它是否有效,两个地址是否相等。地址的大小对于程序逻辑几乎没有影响。</p>
<p>当强调 指针 和 值 的区别时,这里的值,就是指我们关心的,可以直接使用的值。</p>
<blockquote>
<p>实际上,这些区别同样存在于 引用 和 值 之间。只是指针的机制更简单透明,所以用了指针作为讨论的对象。</p>
</blockquote>
<h3 id="不透明引用"><a href="#不透明引用" class="headerlink" title="不透明引用"></a>不透明引用</h3><p>引用(reference)是指可以让程序间接访问其它值的值。<strong>指针是最简单的、透明的引用</strong>,也因为其机制透明和自由使用,是最强大有效的引用。</p>
<p>但透明和自由,也要求使用者更了解底层细节,程序更容易出错。想降低使用难度,避免出错,就加上限制,屏蔽底层细节,变成不透明引用。例如,无法获取引用真实的值,无法控制引用的解释,强制的类型安全,禁止类型转换,甚至让它看起来像一个直接访问的值,不像引用。</p>
<p>当我们将 指针 和 引用 并列时,指的就是不透明引用。</p>
<p>来看看其它语言的情况:</p>
<ul>
<li><p>C++ 既有指针也有引用。C++ 的引用更接近别名(alias),是受限的指针(不能读取或修改地址值,也不需要显式的解引用,所有操作都作用于指向的值)。</p>
</li>
<li><p>Python 和 Java 都取消了指针,只保留了引用。Java 的基本类型是直接值,除此以外都是引用。Python 更彻底,一切皆对象,所有变量都是对象的引用。所以它们在赋值和传递时,没有拷贝对象,只拷贝引用。如果需要拷贝对象,就需要显式地调用拷贝函数或者克隆方法。一些 Python 教程很形象地称这种引用为『贴标签』。</p>
</li>
</ul>
<p><img src="../../images/golang-in-action-day9/value-vs-reference.svg" alt=""></p>
<p>Go 语言的引用,不像一般意义上的引用。</p>
<p>其它语言的不透明引用,是一种<strong>语言级别的统一机制</strong>,是作为指针的替代方案出现的。</p>
<p>Go 的引用,则是在已经有了 直接值 和 指针 的前提下,<strong>针对特定类型的优化</strong>:为了兼顾易用性和性能,针对具体类型,在 值 和 指针 之间折中。每种引用类型,有自己独特的机制。一般是由一个结构体负责管理元数据,结构体里有一个指针,指向真正要使用的目标数据。</p>
<p>这种东西,如果在 C++ 或者 Java 里,就是一个官方提供的类(如 Java 的 <code>String</code> 类),可以看到它的内部机制。而 Go 引用的实现逻辑却内置在 runtime 里,不仅无法直接访问元数据,还表现得像在直接操作目标数据。你会以为它是个普通的值,直到某些行为跟想象中不一样,才想起了解它的底层结构。如果不去看 runtime 的源码,这些元数据结构体仿佛不存在。</p>
<p>Go 的引用类型有:</p>
<ul>
<li><p>字符串 <code>string</code>:底层的数据结构为 <code>stringStruct</code> ,里面有一个指针指向实际存放数据的字节数组,另外还记录着字符串的长度。不过由于 <code>string</code> 是只读类型(所有看起来对 <code>string</code> 变量的修改,实际上都是生成了新的实例),在使用上常常把它当做值类型看待。由于做了特殊处理,它甚至可以作为常量。<code>string</code> 也是唯一零值不为 <code>nil</code> 的引用类型。</p>
</li>
<li><p>切片(slice):底层数据结构为 <code>slice</code> 结构体 ,整体结构跟 <code>stringStruct</code> 接近,只是多了一个容量(capacity)字段。数据存放在指针指向的底层数组里。</p>
</li>
<li><p>映射(map):底层数据结构为 <code>hmap</code> ,数据存放在数据桶(buckets)中,桶对应的数据结构为 <code>bmap</code> 。</p>
</li>
<li><p>函数(func):底层数据结构为 <code>funcval</code> ,有一个指向真正函数的指针,指向另外的 <code>_func</code> 或者 <code>funcinl</code> 结构体(<code>funcinl</code> 代表被行内优化之后的函数)。</p>
</li>
<li><p>接口(interface):底层数据结构为 <code>iface</code> 或 <code>eface</code> (专门为空接口优化的结构体),里面持有动态值和值对应的真实类型。</p>
</li>
<li><p>通道(chan):底层数据结构为 <code>hchan</code>,分别持有一个数据缓冲区,一个发送者队列和一个接收者队列。</p>
</li>
</ul>
<p>这些类型在直接赋值拷贝的时候,都只会拷贝它们的直接值,也就是<strong>元数据结构体</strong>;间接指向的底层数据,是在各个拷贝值之间共享的。除非是发生了类型转换这样的特殊情况。</p>
<p><img src="../../images/golang-in-action-day9/slice-reference.svg" alt=""></p>
<p>如果觉得不好记忆,有一个识别引用类型的快捷办法:凡是零值是 <code>nil</code> 的,都是引用类型。指针作为特殊的透明引用,一般单独讨论。而 字符串 <code>string</code> 因为做了特殊处理,零值为 <code>""</code> ,需要额外记住。除了引用类型和指针,剩下的类型都是直接值类型。</p>
<p>那些说引用类型只有需要 <code>make()</code> 的切片、映射、通道 三种的说法,是<strong>错误</strong>的!</p>
<blockquote>
<p>如果不记得都有哪些类型,零值是什么,可以看第 3 期《<a href="../../2020/golang-in-action-day-3/">类型</a>》。或者看下图的整理:</p>
</blockquote>
<p><img src="../../images/golang-in-action-day9/go-types.png" alt=""></p>
<p>由于每一个类型的实现机制都有所不同,具体细节留到介绍这些类型时再讨论,不在这里展开。感兴趣可以到 <code>go目录/src/runtime</code> 下看源码(每个类型有自己单独的文件,如 <code>string.go</code>,个别没有单独源码的,在 <code>runtime2.go</code> 里面)。</p>
<blockquote>
<p>需要注意的是,Go 通过封装,刻意隐藏引用类型的内部细节。隐藏细节,意味着没有对这些细节作出承诺,这些细节完全可能在后续版本中变更。实际上这样的变更已经发生过。了解这些细节,是为了更好理解类型的一些特殊行为,而不是要依赖于这些细节。(考虑到海勒姆定律,这些细节最终还是会被一些程序依赖。)</p>
<p>由于『引用类型』这个术语边界不明,特别是 Go 的实现方式跟其它语言存在差异,在表述上常常会造成混乱和误解,go101 的作者老貘推荐在 Go 里改为使用『指针持有者类型』来代替。新术语是指一个类型要么本身就是一个指针,要么是一个包裹着指针的结构体,它的变量本身是一个直接值,这个值另外指向间接的值。当赋值或传参发生拷贝时,只拷贝了直接值部分,间接值被多个直接值共享。</p>
<p>这种提法提供了新的理解角度。但我仍然使用『引用类型』这个术语,是想强调这些类型的不透明属性。它们由 runtime 内置,其元数据和实现机制被封装隐藏。按照『指针持有者类型』的定义,我们也可以自行实现一个包裹指针的结构体。但这种结构体跟普通结构体没有什么区别,runtime 不会对它做特殊处理。</p>
</blockquote>
<h2 id="指针传递、引用传递和值传递"><a href="#指针传递、引用传递和值传递" class="headerlink" title="指针传递、引用传递和值传递"></a>指针传递、引用传递和值传递</h2><p>因为指针和引用本质上也是值,字面意义上,Go 里面<strong>所有传递都是值传递</strong>。这句话正确却没有指导意义。</p>
<p>Go 里的赋值和传参,总是会把传递的值<strong>本身</strong>拷贝一份。但如果这个(直接)值指向别的(间接)值,它所指向的(间接)值不会发生递归拷贝。就好比把大门钥匙多配一把交出去,而不是新建一模一样的房子。</p>
<p>因为这个特性,加上前面介绍的 直接值 、不透明引用 和 指针 的区别,这三种传递在使用上是有区别的。区分也很简单,赋值和参数的类型是什么类型,就是对应的传递方式。</p>
<ul>
<li><p>(直接)值传递:值发生了拷贝。对新值的任何修改,都不会影响原来的值。</p>
<p>除非这个值是一个结构体,结构体成员字段里有引用类型或者指针,那么对这个字段而言,则是引用传递/指针传递。</p>
</li>
<li><p>引用传递:元数据发生了拷贝,但底层的间接值没有拷贝,仍然共享。</p>
<ul>
<li><p>对间接值的修改,会影响所有副本。(如,修改切片里的某个元素,就是修改了底层数组里的某个元素)</p>
</li>
<li><p>但对元数据的修改则不会影响其它副本。(如,对切片提取子切片,实际上修改了切片的访问范围)</p>
</li>
<li><p>有一种特殊的情况,就是修改元数据时改变了指向的间接值的指针,这之后对间接值的修改,都不再会影响其它副本。因为不再共享间接值。(如,对切片追加元素时,促发了底层数组的重新分配,指向了新的底层数组)</p>
</li>
</ul>
</li>
<li><p>指针传递:指针值(地址)发生了拷贝,共享指向的值。对间接值的修改,会影响所有副本。由于 Go 不允许对指针进行运算,不存在意外改变指针的情况。而如果是给指针赋新的值,后续的修改当然不再影响旧值指向的值。由于指针的机制透明,这点很好理解。</p>
</li>
</ul>
<p>因为指针本身也是一种引用,本来指针和引用可以合并讨论。但由于引用屏蔽了实现细节,使得程序员不一定知道对引用的操作,作用的具体是哪一部分,也就比透明的指针多了更多的意外情况需要指出。</p>
<h2 id="练习题"><a href="#练习题" class="headerlink" title="练习题"></a>练习题</h2><p>以下代码有 8 个真假判断,请在不运行的情况下,判断 true 还是 false,并说出理由。</p>
<figure class="highlight go"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div><div class="line">20</div><div class="line">21</div><div class="line">22</div><div class="line">23</div><div class="line">24</div><div class="line">25</div><div class="line">26</div><div class="line">27</div><div class="line">28</div><div class="line">29</div><div class="line">30</div><div class="line">31</div><div class="line">32</div><div class="line">33</div><div class="line">34</div><div class="line">35</div><div class="line">36</div><div class="line">37</div><div class="line">38</div><div class="line">39</div><div class="line">40</div><div class="line">41</div><div class="line">42</div><div class="line">43</div><div class="line">44</div><div class="line">45</div><div class="line">46</div><div class="line">47</div><div class="line">48</div><div class="line">49</div><div class="line">50</div><div class="line">51</div><div class="line">52</div><div class="line">53</div><div class="line">54</div><div class="line">55</div><div class="line">56</div></pre></td><td class="code"><pre><div class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span>{</div><div class="line"> <span class="keyword">var</span> a1 = [<span class="number">5</span>]<span class="keyword">int</span>{<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>, <span class="number">5</span>}</div><div class="line"> <span class="keyword">var</span> a2 = a1</div><div class="line"> a2[<span class="number">0</span>] = <span class="number">99</span></div><div class="line"> <span class="comment">//fmt.Println(a1, a2)</span></div><div class="line"> fmt.Println(<span class="string">"a1[0]==a2[0]? "</span>, a1[<span class="number">0</span>] == a2[<span class="number">0</span>])</div><div class="line"></div><div class="line"> <span class="keyword">var</span> sa = a1[:]</div><div class="line"> sa[<span class="number">1</span>] = <span class="number">88</span></div><div class="line"> <span class="comment">//fmt.Println(a1, sa)</span></div><div class="line"> fmt.Println(<span class="string">"a1[1]==sa[1]? "</span>, a1[<span class="number">1</span>] == sa[<span class="number">1</span>])</div><div class="line"></div><div class="line"> <span class="keyword">var</span> s1 = []<span class="keyword">int</span>{<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>, <span class="number">5</span>}</div><div class="line"> <span class="keyword">var</span> s2 = s1</div><div class="line"> s2[<span class="number">0</span>] = <span class="number">99</span></div><div class="line"> <span class="comment">//fmt.Println(s1, s2)</span></div><div class="line"> fmt.Println(<span class="string">"s1[0]==s2[0]? "</span>, s1[<span class="number">0</span>] == s2[<span class="number">0</span>])</div><div class="line"></div><div class="line"> <span class="keyword">var</span> s3 = s2[<span class="number">1</span>:<span class="number">4</span>]</div><div class="line"> s3[<span class="number">0</span>] = <span class="number">88</span></div><div class="line"> <span class="comment">//fmt.Println(s1, s3)</span></div><div class="line"> fmt.Println(<span class="string">"s1[1]==s3[0]? "</span>, s1[<span class="number">1</span>] == s3[<span class="number">0</span>])</div><div class="line"></div><div class="line"> <span class="keyword">var</span> s4 = <span class="built_in">append</span>(s2, <span class="number">6</span>)</div><div class="line"> s4[<span class="number">2</span>] = <span class="number">77</span></div><div class="line"> <span class="comment">//fmt.Println(s1, s4)</span></div><div class="line"> fmt.Println(<span class="string">"s1[2]==s4[2]? "</span>, s1[<span class="number">2</span>] == s4[<span class="number">2</span>])</div><div class="line"></div><div class="line"> <span class="keyword">var</span> oldLen <span class="keyword">int</span></div><div class="line"></div><div class="line"> oldLen = <span class="built_in">len</span>(s1)</div><div class="line"> <span class="comment">//fmt.Println(s1)</span></div><div class="line"> appendInt(s1, <span class="number">6</span>)</div><div class="line"> <span class="comment">//fmt.Println(s1)</span></div><div class="line"> fmt.Println(<span class="string">"len(s1)==oldLen+1?"</span>, <span class="built_in">len</span>(s1) == oldLen+<span class="number">1</span>)</div><div class="line"></div><div class="line"> oldLen = <span class="built_in">len</span>(s1)</div><div class="line"> <span class="comment">//fmt.Println(s1, s2)</span></div><div class="line"> appendIntPtr(&s2, <span class="number">6</span>)</div><div class="line"> <span class="comment">//fmt.Println(s1, s2)</span></div><div class="line"> fmt.Println(<span class="string">"len(s1)==oldLen+1?"</span>, <span class="built_in">len</span>(s1) == oldLen+<span class="number">1</span>)</div><div class="line"></div><div class="line"> oldLen = <span class="built_in">len</span>(s1)</div><div class="line"> <span class="comment">//fmt.Println(s1)</span></div><div class="line"> appendIntPtr(&s1, <span class="number">6</span>)</div><div class="line"> <span class="comment">//fmt.Println(s1)</span></div><div class="line"> fmt.Println(<span class="string">"len(s1)==oldLen+1?"</span>, <span class="built_in">len</span>(s1) == oldLen+<span class="number">1</span>)</div><div class="line">}</div><div class="line"></div><div class="line"><span class="function"><span class="keyword">func</span> <span class="title">appendInt</span><span class="params">(s []<span class="keyword">int</span>, elems ...<span class="keyword">int</span>)</span></span> {</div><div class="line"> s = <span class="built_in">append</span>(s, elems...)</div><div class="line">}</div><div class="line"></div><div class="line"><span class="function"><span class="keyword">func</span> <span class="title">appendIntPtr</span><span class="params">(ps *[]<span class="keyword">int</span>, elems ...<span class="keyword">int</span>)</span></span> {</div><div class="line"> *ps = <span class="built_in">append</span>(*ps, elems...)</div><div class="line">}</div></pre></td></tr></table></figure>
<ul>
<li><p>满分:无需运行代码,全部判断正确。</p>
</li>
<li><p>优秀:有个别判断不确定,但看到运行结果可以推断出原因。</p>
</li>
<li><p>及格:有比较多的判断不确定,但在输出数组/切片元素(注释掉的代码行)之后能说出原因。</p>
</li>
<li><p>加把劲:即使看到元素输出,还是云里雾里。</p>
</li>
</ul>
<p>对于从头开始学习的朋友来说,即使感觉云里雾里也不要紧,因为练习题不可避免地涉及到下一期要讨论的 <strong>数组 和 切片</strong>。如果之前没有了解,判断不了也是正常。这道题既是这期的课后练习,也可以理解为下期的课前预习。</p>
<p>答案和解析会在下期公布。</p>
<h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ul>
<li>Value-英文维基:<a href="https://en.wikipedia.org/wiki/Value_(computer_science" target="_blank" rel="external">https://en.wikipedia.org/wiki/Value_(computer_science</a>)</li>
<li>Reference-英文维基:<a href="https://en.wikipedia.org/wiki/Reference_(computer_science" target="_blank" rel="external">https://en.wikipedia.org/wiki/Reference_(computer_science</a>)</li>
<li>Pointer-英文维基:<a href="https://en.wikipedia.org/wiki/Pointer_(computer_programming" target="_blank" rel="external">https://en.wikipedia.org/wiki/Pointer_(computer_programming</a>)</li>
<li>值部-Go语言101:<a href="https://gfw.go101.org/article/value-part.html" target="_blank" rel="external">https://gfw.go101.org/article/value-part.html</a></li>
</ul>
<hr>
<p><img src="https://i.creativecommons.org/l/by-nc-sa/4.0/88x31.png" alt="知识共享 “署名-非商业性使用-相同方式共享” 4.0 (CC BY-NC-SA 4.0)”许可协议"><br>本文为本人原创,采用<a href="http://creativecommons.org/licenses/by-nc-sa/4.0/" target="_blank" rel="external">知识共享 “署名-非商业性使用-相同方式共享” 4.0 (CC BY-NC-SA 4.0)”许可协议</a>进行许可。<br>本作品可自由复制、传播及基于本作品进行演绎创作。如有以上需要,请留言告知,在文章开头明显位置加上署名(Jayce Chant)、原链接及许可协议信息,并明确指出修改(如有),不得用于商业用途。谢谢合作。<br>请点击查看<a href="http://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh" target="_blank" rel="external">协议</a>的中文摘要。</p>
]]></content>
<summary type="html">
<p>在经过编写 CLI 程序的尝试之后,我们继续回来聊 Go 语言的基础知识。</p>
<p>相信实际写过一些代码之后,会更容易理解。</p>
</summary>
<category term="Go 语言实战" scheme="https://jaycechant.info/categories/Go-%E8%AF%AD%E8%A8%80%E5%AE%9E%E6%88%98/"/>
<category term="golang" scheme="https://jaycechant.info/tags/golang/"/>
</entry>
<entry>
<title>18 公斤菠萝怎么吃?来个焦糖风味吧</title>
<link href="https://jaycechant.info/2021/pineapple-with-caramel-flavor/"/>
<id>https://jaycechant.info/2021/pineapple-with-caramel-flavor/</id>
<published>2021-03-28T07:03:20.000Z</published>
<updated>2021-05-06T17:13:52.478Z</updated>
<content type="html"><![CDATA[<p>前段时间,海关总署发布通知,由于自 2020 年以来台湾菠萝多个批次检测出有害生物,暂停台湾地区的菠萝入境。</p>
<a id="more"></a>
<blockquote>
<p>本文写于 3 月,只是因为各种原因一直没写完发表。</p>
<p>太长不看</p>
<ul>
<li>凤梨就是菠萝,没有任何区别。叫法的区分更多是商业上的品牌营销。</li>
<li>菠萝扎嘴可能是蛋白酶、草酸钙结晶和粗纤维划伤综合后的结果。</li>
<li>但无论是哪种因素,泡盐水都起不了什么作用。</li>
<li>除了买熟透的菠萝,最好的办法是加热。除了做成佳肴,还可以选择炙烤,给菠萝增加一道焦糖风味。</li>
</ul>
</blockquote>
<h2 id="18-公斤不能当真"><a href="#18-公斤不能当真" class="headerlink" title="18 公斤不能当真"></a>18 公斤不能当真</h2><p>事情发生之后,大陆这边影响不大,因为本身海南和广东很多地方都产菠萝。其中徐闻占据国内约 40% 的产量。徐闻菠萝还趁热度做了一波营销,知名度和价格都打了翻身仗。</p>
<p>但对台湾地区来说,影响就比较大。销往大陆的菠萝,虽然只占产量的10%,占总贸易量的比例也不大,却占据菠萝外销量的 97%。被禁会对菠萝价格产生很大的冲击,首当其冲受到影响的就是果农的收入。岛内媒体讨论得热火朝天,然后就有『名嘴』说出了『每人每天18公斤,四五天就解决了』的知名言论。</p>
<p>这种言论当然不能当真。稍微了解过对岸媒体言论的人都知道,因为媒体竞争激烈,加上没有相应的管治措施,那些看似正经的电视节目上都总是语不惊人誓不休,为收视率什么话都敢说。这些节目更接近大陆这边的朋友圈标题党,而不是看起来对标的卫视节目,明白这点就好理解多了。</p>
<p>虽然言论不靠谱,但作为一个菠萝爱好者,就让我也标题党一把,蹭蹭这(过时的)热度,聊聊菠萝该怎么吃。</p>
<h2 id="菠萝?凤梨?"><a href="#菠萝?凤梨?" class="headerlink" title="菠萝?凤梨?"></a>菠萝?凤梨?</h2><p>你可能会说,等等,台湾的不是<strong>凤梨</strong>吗?</p>
<p>台湾产的是凤梨,也是菠萝。因为凤梨就是菠萝,两者没有本质区别。</p>
<p>你说不对啊,我看网上的文章说,菠萝有『钉』,凤梨『钉』很小或没有;菠萝『扎嘴』要泡盐水,凤梨不用泡……</p>
<p><img src="../../images/pineapple/boluo-and-fengli-PC.png" alt="电脑上搜索两者的区别"></p>
<p><img src="../../images/pineapple/boluo-and-fengli-weichat.png" alt="微信上搜索两者的区别"></p>
<p>说来话长,让我『稍微』解释一下。</p>
<h3 id="水果中贵族南美菠萝"><a href="#水果中贵族南美菠萝" class="headerlink" title="水果中贵族南美菠萝"></a>水果中贵族南美菠萝</h3><p>菠萝是属于禾本目(Poales) 凤梨科(Bromeliaceae) 凤梨属(Ananas) 菠萝种(Ananas comosus) 植物的果实,原产南美洲。在欧洲人踏足南美洲之前,菠萝已经有多个世纪的种植历史。据《拉鲁斯美食大全》所说,菠萝在 15 世纪被哥伦布带回欧洲。也有说法菠萝在 16 世纪之后才传到欧洲(哥伦布死于 1506 年,也就是16 世纪初)。</p>
<p>无论菠萝是不是由哥伦布本人或他的船队带回欧洲,菠萝传播到欧洲大概率还是可以归功于哥伦布的航行。自哥伦布发现新大陆之后,地球上各个大陆之间的交流剧增,这里面包括自然生物、农作物、人种、文化、传染病乃至思想观念,对世界历史的走向产生了深远的影响,史称『哥伦布大交换』。</p>
<p>很多我们早已习以为常的作物,包括玉米、辣椒、番茄、番薯(地瓜)、棉花、花生、(番)木瓜、土豆、南瓜、草莓、烟草……(名单太长不列了),就是在这个过程里传到欧亚大陆。(我看宋代的剧为什么会看到玉米地啊(╯‵□′)╯︵┻━┻)</p>
<p>由于欧洲的气候并不适宜种植菠萝,在很长一段时间里,菠萝是只有皇室贵族才有机会接触到的水果,代表着异国情调、权力与财富。这也是菠萝在欧洲传播非常缓慢的原因,使得菠萝在欧洲不同地方的记载,传入时间差距很大。即使后面好不容易发明了温室种植法,菠萝的种植成本依然很高(温室内要一直烧炭火保温保湿),只是从贵族扩展到了富人阶层。</p>
<p>这就导致了一些现在看来匪夷所思的行为。各种绘画、建筑、装饰里出现菠萝作为权力和财富的象征,还好理解;皇室和贵族在宴席上放几个菠萝,但往往只是装饰,并一定会吃掉,也就算了。为了模仿皇室的宴会,普通人开始租(对,你没看错,租)菠萝在宴席上撑场面,吃是不可能吃的,吃掉了拿什么还。</p>
<p>直到现在,新鲜菠萝在欧洲很多地方还是少而贵,属于小奢侈的水果。日本的情况也类似。小时候看动画,主人公往往要碰上重要场合收到菠萝当礼物(很多年没见的叔叔来看我),才有机会吃到一个菠萝,吃的过程非常郑重,充满了仪式感。</p>
<p>我生长于广东十八线小县城,菠萝成熟的时候,街上到处有小摊贩在卖,几块钱一个,几毛钱一块,即使当时家里比较拮据,偶尔几块菠萝还是吃得起的。怎么发达国家反而吃不起的样子?对此非常疑惑。</p>
<h3 id="波罗与番梨"><a href="#波罗与番梨" class="headerlink" title="波罗与番梨"></a>波罗与番梨</h3><p>按网上找到的说法,菠萝大概是跟随葡萄牙人,在明末 16 到 17 世纪之间传到广东。流传最广的说法,是先到澳门,再往不同方向传播,在清朝康熙年间传到台湾。</p>
<p>因为传播路径复杂,又是舶来品,在结合当地人的认知和方言之后,菠萝有了一大堆的名字。抛开那些故纸堆里的(疑似)曾用名,现在还在用的名字有 菠萝(粤语)、番梨(潮州话)、黄梨(客家话、东南亚华人)、王梨(闽南地区,谐音旺来)、凤梨(台湾地区)。</p>
<p>这些名字可以很容易地分成两类:菠萝,以及 X梨。对于 某梨的叫法,虽然第一个字不同,但在各自的方言里,发音都非常接近。我们可以合理地推测,这两类命名是两个不同传播路径产生的。</p>
<p>在粤语(更准确说是广府话)地区以及往西传播的过程中,明显是因为长得有点像,参考了同样是舶来品的波罗蜜(隋唐时从印度传入中国,叫『婆那娑』,宋改称波罗蜜,这明显是受到了佛教的影响),称作波罗,后来强调是植物,加上了草字头。有趣的是,后来菠萝在广东变得更为普遍,波罗蜜反而少见一些,有些地方开始称波罗蜜为『树菠萝』。不过波罗蜜仍然是正式名称。</p>
<p>而在一系列 X梨的路径里,无论从传播顺序,还是含义的演变看,番梨都更像是开始的命名。外来的事物,称番,像番茄、番石榴、番木瓜。但这个番字,在天朝上国华夷之辩的思想里,暗含贬义。在继续传播的路上,越来越本土化,大家会有意无意回避『番』字,给出相近发音下,其它『合理化』的雅称:果肉是<strong>黄</strong>色、吃了<strong>旺</strong>来、叶子像<strong>凤</strong>尾……</p>
<p>当然,也可能不是这样的顺序。但相邻的方言区,不约而同地用相近的发音称呼,不管是谁影响谁,无疑是一个来源。另一个证据是,包括台湾的地方府志在内,历史上这些地区的记录里,菠萝的称呼并不固定,几个叫法都有出现过。台湾到了后面才逐渐固定使用凤梨这个名字。</p>
<p>大陆这边,在罐头生产和生鲜运输把菠萝送出两广(包括建省前的海南)和福建地区之前,其他大部分地区对这个水果是没有概念的。到后来全国各地可以吃到菠萝的时候,一方面粤语区的产量比较大,另一方面珠三角的经济发达影响力强,菠萝就逐渐成为通用的中文名。</p>
<p>菠萝在分类学上的科和属,是直接借用日本学者定的科名、属名。而对于日本来说,凤梨科的植物都是外来物种,命名明显受到台湾地区的影响。</p>
<h3 id="城里的Jennifer,也是村里的翠花"><a href="#城里的Jennifer,也是村里的翠花" class="headerlink" title="城里的Jennifer,也是村里的翠花"></a>城里的Jennifer,也是村里的翠花</h3><p>如果只是到这里,那么<strong>菠萝、番梨、黄梨、王梨</strong>和<strong>凤梨</strong>,都只是同一种水果在不同地方的俗称。此时的称呼差别,更多与民俗方言相关,并非以海峡为界,更没有没有涉及品种差异。除了粤语区比较固定地称为菠萝,粤东、闽南和台湾整个广义的闽南方言区,几个 X梨 的称呼很长一段时间都有混用。引种东南亚品种,还有各种品种改良,是在不同称呼之后的事情。</p>
<p>潮汕-闽南地区,同时受到两边的影响,既有跟粤语区叫菠萝的地方,也有叫王梨、凤梨的时候;连海峡对岸,也有称呼菠萝的时候。而脱胎于闽南式婚嫁礼饼的凤梨酥,无论在岛内还是在闽南地区,无论馅料用的凤梨还是菠萝,都不妨碍它按照习惯叫凤梨酥。</p>
<p>直到后来,台湾经过多年的农业技术研究,培育出多个高甜少刺的特色菠萝品种,才开始专门区分,将这些品种称为凤梨,而将进口菠萝和纤维较粗的『开英种』和『本岛仔凤梨』称为菠萝。这更多是一种出于商业考量的品牌打造,无可厚非。早年台湾地区比大陆发达,是大陆羡慕的对象,将『凤梨』跟高甜少刺、发达地区的高大上绑定,以售出高价,是成功的营销。但为了保持这种高大上,刻意区分,否认凤梨也是菠萝,就属于混淆视听了。</p>
<p>这跟『樱桃』和『车厘子』还不完全一样。因为它们两者是同为李属(Prunus) 下的不同种(species),我们一般所说的樱桃指『中国樱桃』(Prunus pseudocerasus) ,而车厘子指『欧洲甜樱桃』(Prunus avium)。它们分别都是当地的原生物种,刻意区分可以理解,不光是翻译的差异。</p>
<p>菠萝和凤梨更接近『猕猴桃』和『奇异果』的关系。中国就是猕猴桃的原生地。奇异果则是新西兰人用湖北带回的种子,一代代培育而成的。经过培育之后,奇异果的差异已经比较大,商业上当然可以专称以示区分,但不可以否认奇异果就是猕猴桃的一个品种(breed)。</p>
<h2 id="吃肉的水果"><a href="#吃肉的水果" class="headerlink" title="吃肉的水果"></a>吃肉的水果</h2><p>相信你已经明白,凤梨就是菠萝。只是叫凤梨的,大概率是台湾的品种。大陆也有自己的改良品种,也有引种台湾的优良品种,视乎宣传需要,有叫菠萝的,也有叫凤梨的。</p>
<p>那为什么有些菠萝吃之前要泡盐水,有些不用呢?</p>
<p>大家的回答普遍是:『扎嘴』,泡盐水可以缓解。</p>
<p>要是你继续追问,为什么会扎嘴,又为什么泡盐水可以缓解,大家就不怎么答得上来了。</p>
<h3 id="扎嘴的是什么"><a href="#扎嘴的是什么" class="headerlink" title="扎嘴的是什么"></a>扎嘴的是什么</h3><p>实际上,就这么『简单』的问题,也没有一个非常确切的答案。根据可以查到的资料,扎嘴的原因按出现频率从高到低分别是:</p>
<h4 id="菠萝蛋白酶"><a href="#菠萝蛋白酶" class="headerlink" title="菠萝蛋白酶"></a>菠萝蛋白酶</h4><p>这是最广泛的说法。大部分情况下也是唯一的解答。</p>
<p>菠萝含有多种蛋白酶,可以分解蛋白质。我们的细胞组织也是由蛋白质构成,其中包括黏膜细胞。所以吃完菠萝之后,蛋白酶会分解一小部分接触到的蛋白质,造成我们的舌面和口腔黏膜损伤。形象地说,就是我在吃菠萝的时候,菠萝也在吃我。</p>
<p>菠萝本身糖分和有机酸含量很高,又会进一步刺激到这些小伤口,于是就会产生刺痛感。</p>
<p>除了菠萝以外,很多水果都含有蛋白酶,包括木瓜、猕猴桃等。利用这个特性,用果汁腌肉有嫩肉的效果。市售的嫩肉粉,一部分的原材料就是木瓜粉。</p>
<p>如果腌肉时放几块新鲜的菠萝,一不小心忘冰箱里过夜,那么第二天很可能会得到一份糨糊。</p>
<h4 id="草酸钙结晶"><a href="#草酸钙结晶" class="headerlink" title="草酸钙结晶"></a>草酸钙结晶</h4><p>有人说,不对啊,酶反应需要时间,但我吃菠萝是入口就感觉到刺痛。另外,有蛋白酶的水果不在少数,为什么很少听说别的水果扎嘴呢?</p>
<p>于是有人提出了另外一种可能:菠萝中含有的草酸钙(针状)结晶造成了刺痛。与此类似还有菠菜和芋头。我没生吃过菠菜和芋头,但是削过芋头后那手确实又痒又痛。</p>
<h4 id="粗纤维"><a href="#粗纤维" class="headerlink" title="粗纤维"></a>粗纤维</h4><p>可以划伤刺痛黏膜的,除了草酸钙针晶,还有可能是一些菠萝品种的粗纤维。</p>
<p>以上三点,究竟哪个是真正/主要原因,至少我没有看到一个决定性的结论。我和稀泥地倾向于,都起了一定的作用,但是所占比例未知。</p>
<p>毕竟扎嘴是一个很主观的感觉,每个人对不同因素造成的扎嘴耐受程度也不同。</p>
<h2 id="不要扎嘴"><a href="#不要扎嘴" class="headerlink" title="不要扎嘴"></a>不要扎嘴</h2><p>受益于良种选育和保鲜技术的发展,现在的菠萝含有更少的蛋白酶和草酸钙,果肉纤维更细,扎嘴的困扰就会少一些。</p>
<p>想不扎嘴,首先就要挑选这些不扎嘴的名优品种。这样看,贵价的凤梨貌似还是有一些价值的。我也见过网上有卖主打不用泡盐水直接吃的手撕菠萝。不过我没试过,大家自己决定要不要相信。</p>
<p>其次,尽量挑选成熟的菠萝,蛋白酶、草酸钙和粗纤维会少一些。一个技巧是,其它条件一致的前提下,尽量挑选产地比较近,运输时间短的产品,因为运输时间越长,果农越是需要提前采摘,让果实在运输途中放熟而不是放烂。放熟和自然成熟差别还是比较明显的。</p>
<p>但如果说因为条件限制,没得挑,菠萝已经买好了,还有没有办法降低扎嘴的程度呢?泡盐水吗?</p>
<p>很遗憾,盐水的作用可能只是聊胜于无。</p>
<p>酶首先是一种蛋白质(但它不会分解自己)。要想蛋白酶失活,可以是加热到一定温度,可以是加入重金属,或者其他毒素、强辐射等。</p>
<p>单靠氯化钠溶液的话,首先需要浓度很高,至少需要 7% 以上,这样的盐水需要非常咸。其次需要泡很长时间,让盐水渗透到果肉内部,而不是停留在表面。</p>
<p>最关键的是,蛋白酶仅仅是因为盐析作用溶解度降低而无法作用,这个过程是可逆的。只要盐水的浓度下降(例如被你分泌的唾液稀释),蛋白酶又可以重新起作用。</p>
<p>而对于草酸钙结晶和粗纤维,我完全看不出盐水能起什么作用。可能就是一个生理盐水缓解不适的作用。</p>
<p>于是乎就出现了,盐水不是用来防扎嘴,而是用来凸显甜度的言论。你看,玄学开始出现了。</p>
<h3 id="不如加热"><a href="#不如加热" class="headerlink" title="不如加热"></a>不如加热</h3><p>既然盐水不靠谱,又不能往食物里下毒,最简单有效的方法其实是:加热!</p>
<p>大部分蛋白加热到 70 度以上 5 分钟(或者更高温度并缩短时间),都会发生不可逆的变性。这里面当然包括娇贵得很的蛋白酶。</p>
<p>草酸钙也会在高温下失去结晶水,变成无水草酸。(不过这个温度要高一些,可能需要达到 100 度)</p>
<p>至于粗纤维,一般的加热是没什么效果的。如果不巧买了一个像甘蔗一般的菠萝,又不想丢弃,可以试着用高压锅压一下。</p>
<p>一旦打开了加热这道大门,路子就宽多了。</p>
<p>加热过后不仅不必再担心扎嘴问题,原本硬邦邦的果肉也会变得柔软,味道得以浓缩,甚至还能在烹饪过程中加入各种风味,乃至成为一道甜品或者佳肴。</p>
<h4 id="炙烤"><a href="#炙烤" class="headerlink" title="炙烤"></a>炙烤</h4><p>直接吃的情况,考虑到水煮会稀释菠萝的风味,推荐你直接切片<strong>炙烤</strong>。</p>
<p><img src="../../images/pineapple/roasted-pineapple-at-home.jpg" alt="在家用电烤箱烤菠萝"></p>
<blockquote>
<p>家用电烤箱温度往往不够高,导致加热时间过长,没办法产生焦糖风味不说,还会出很多水。</p>
<p>条件允许还是更建议用铸铁锅烤(grilled),没有的话,用平底锅大火煎也可以。</p>
</blockquote>
<p>不仅不会稀释,还会在烤的过程中让菠萝失水,让风味浓缩;如果温度合适,甚至还会产生迷人的<strong>焦糖香气</strong>。</p>
<p>汉堡王就曾经推出一系列烤菠萝片的产品,风味非常独特。</p>
<p><img src="../../images/pineapple/burgerking-grilled-pineapple.png" alt="汉堡王的烤菠萝"></p>
<p><img src="../../images/pineapple/burgerking-pineapple-burger.jpg" alt="汉堡王的烤菠萝汉堡"></p>
<p>需要提醒的是,舌头在高温下对甜味比较迟钝。菠萝有机酸含量很高,烤过的菠萝趁热吃,会因为甜味变得不明显,而显得非常酸。耐酸的朋友不妨趁热尝试一下这独特的风味。怕酸的朋友则不妨等放凉乃至冷藏之后再吃,并不会折损它浓缩过的风味。</p>
<h4 id="肉桂"><a href="#肉桂" class="headerlink" title="肉桂"></a>肉桂</h4><p>直接炙烤只是入门。如果你能接受西式甜品里肉桂和糖分碰撞的味道,那不妨在高温的烤架或者铸铁锅上,再撒上一小撮肉桂粉。</p>
<p>除此以外,其它西式甜点里会用到的香料,都不妨拿来试一下。其实就是参考菜谱,把菠萝当成甜品的主角去装扮。</p>
<h4 id="入菜"><a href="#入菜" class="headerlink" title="入菜"></a>入菜</h4><p>如果觉得西式甜品的香料过于黑暗无法接受,那不妨试试粤菜里对菠萝的用法。</p>
<p>随便一搜菜谱,菠萝炒饭、菠萝咕噜肉、菠萝炒肉、菠萝糯米饭、菠萝派……总有一款适合你。</p>
<h2 id="18-公斤真的不行"><a href="#18-公斤真的不行" class="headerlink" title="18 公斤真的不行"></a>18 公斤真的不行</h2><p>菠萝酸甜可口,香味浓郁,又有那么多膳食纤维、维生素和微量元素,我真的非常喜欢吃,直接秒掉一整个都毫无压力。</p>
<p>但是如果让我一天吃掉 18 公斤,那是不可能的,这辈子都不可能一下子吃 18 公斤的,加上焦糖风味也不行。</p>
<p>首先是蛋白酶的问题。少量蛋白酶问题不大,到消化道碰上胃酸就变性失活了,只是嘴巴遭点罪。可是如果是连续几公斤菠萝的蛋白酶,就不是一回事了,吃得太多可能会引起消化道溃疡。</p>
<p>你说看完这篇文章,学会了加热再吃,不怕蛋白酶。草酸钙仍然是一个问题,加热只是破坏了结晶,草酸本身还在。一下子摄入过多草酸,会引起各种健康问题。一个最直接的后果,是可能引起高草酸尿症,继而引起急性肾损伤,严重时可以致命。</p>
<p>除此以外,菠萝的含糖量非常高,每 100g 菠萝含糖量可以达到 12g 以上,比很多水果都要高,只是因为有机酸含量也高,让它尝起来没那么甜。短时间内吃太多菠萝,等于摄入大量糖分,会引起各种健康问题。</p>
<p>所以,菠萝虽好,不能贪吃啊。</p>
<hr>
<p>本文为本人原创,采用<a href="http://creativecommons.org/licenses/by-nc-sa/4.0/" target="_blank" rel="external">知识共享 “署名-非商业性使用-相同方式共享” 4.0 (CC BY-NC-SA 4.0)”许可协议</a>进行许可。 本作品可自由复制、传播及基于本作品进行演绎创作。如有以上需要,请留言告知,在文章开头明显位置加上署名(Jayce Chant)、原链接及许可协议信息,并明确指出修改(如有),不得用于商业用途。谢谢合作。 请点击查看<a href="http://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh" target="_blank" rel="external">协议</a>的中文摘要。</p>
]]></content>
<summary type="html">
<p>前段时间,海关总署发布通知,由于自 2020 年以来台湾菠萝多个批次检测出有害生物,暂停台湾地区的菠萝入境。</p>
</summary>
</entry>
<entry>
<title>Go 语言格式化动词</title>
<link href="https://jaycechant.info/2021/golang-format-verbs/"/>
<id>https://jaycechant.info/2021/golang-format-verbs/</id>
<published>2021-03-14T16:43:36.000Z</published>
<updated>2021-03-24T07:33:19.167Z</updated>
<content type="html"><![CDATA[<p>这期算是 《<a href="../../categories/Go-语言实战/">Go 语言实战</a> 》的番外,内容以翻译整理为主。</p>
<a id="more"></a>
<p>它不在原本的规划之内。</p>
<p>但随着内容的深入,程序变得越来越复杂,我们将不可避免地会遇到 bug,需要调试,需要(往 console 或 日志)输出调试信息。这时数据的格式化输出变得尤为重要。</p>
<p>实际上,前面已经多次用到了格式化。与其每次用到零碎地介绍,不如集中一期整理好。</p>
<p>介绍、翻译、注释、举例,内容有点多,不必全篇记忆。记住常用部分,剩下的留个印象,需要时回来翻阅就好。</p>
<p>[TOC]</p>
<h2 id="fmt-包"><a href="#fmt-包" class="headerlink" title="fmt 包"></a>fmt 包</h2><p>格式化的功能,主要在 <code>fmt</code> 包内,<code>fmt</code> 是 <strong>format</strong> 的略写。</p>
<p>当然,除了临时的简单调试,直接用 <code>fmt</code> 输出到终端(terminal)来调试不太规范。标准输出的内容非常容易丢失,还是写入日志文件方便事后对比分析。</p>
<p>更多的时候,我们会用各种日志库来输出。但这些日志库,要么底层还是调用了 <code>fmt</code> ,要么自己实现的格式化也会尽量和 <code>fmt</code> 兼容。所以学习格式化仍然是必要的。下面主要的内容均来自 <code>fmt</code> 包。</p>
<h3 id="输出-Printing"><a href="#输出-Printing" class="headerlink" title="输出 Printing"></a>输出 Printing</h3><blockquote>
<p>注:print 对应的中文翻译应为 印刷、打印。</p>
<p>但在当前上下文中,print 并非指将内容打印到纸张等介质。而是指的是将各种数据,按照某种格式,转换为字符序列(并输出到抽象文件)的过程。</p>
<p>所以为了方便理解,我将其替换成了『输出』,请读者知悉。</p>
</blockquote>
<p><code>fmt</code> 包中名字里带 <code>Print</code> 的函数很多,但无非是两个选项的排列组合。理解了每个部分的含义,一眼就能明白函数的用途。</p>
<p>前缀代表输出目标:</p>
<ol>
<li><p><code>Fprint</code> 中前缀 <code>F</code> 代表 <strong>file</strong> ,表示内容 <strong>输出到文件</strong>。</p>
<p> 当然这里的文件是抽象的概念,实际对应的是 <code>io.Writer</code> 接口。<code>Fprint</code> 开头的函数,第一个参数总是 <code>io.Writer</code>。通过传递不同的文件给函数,可以把内容输出到不同的地方。</p>
<p> 常见的用法,是打开一个文件,将文件对象作为第一个参数,将内容输出到该文件。当然,不要被 <strong>文件</strong> 这个词误导了,抽象的文件可以是任意的字节流(stream)。具体到这里,只要是可写入的对象(带 <code>Write([]byte)(int, error)</code> 方法),都满足 <code>io.Writer</code> 接口。</p>
</li>
<li><p><code>Print</code> (没有前缀)表示内容 <strong>输出到标准输出</strong>,也就是 控制台(console)或者叫终端(terminal)。</p>
<p> 实际上调用的是 <code>Fprint(os.Stdout, a...)</code> ,换言之背后指定输出的文件为标准输出。</p>
</li>
<li><p><code>Sprint</code> 中前缀 <code>S</code> 表示 <strong>string</strong> ,表示内容 <strong>输出到字符串</strong> ,然后将字符串返回。</p>
</li>
</ol>
<p>后缀表示格式:</p>
<ol>
<li><p><code>Print</code> (没有后缀),表示输出时格式不进行额外的处理。</p>
<p> 也就是按参数的 <strong>默认格式</strong> ,顺序输出。</p>
</li>
<li><p><code>Println</code> 的后缀 <code>ln</code> 代表 <strong>line</strong> ,表示按行输出。</p>
<p> 实际上它只是比 <code>Print</code> 的多做两件事:所有参数之间增加一个空格;输出的最后会追加一个换行符。</p>
</li>
<li><p><code>Printf</code> 的后缀 <code>f</code> 代表 <strong>format</strong> ,表示格式化输出。</p>
<p> 第一个参数是 <strong>格式化字符串</strong> ,通过里面的 <strong>格式化动词(verb)</strong> 来控制后续参数值的输出格式。</p>
</li>
</ol>
<p>直接看代码:</p>
<figure class="highlight go"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div></pre></td><td class="code"><pre><div class="line">fmt.Print(<span class="string">"Print:"</span>, <span class="number">1</span>, <span class="string">"two"</span>, <span class="number">3.0</span>, <span class="string">"over."</span>)</div><div class="line">fmt.Println(<span class="string">"Println:"</span>, <span class="number">1</span>, <span class="string">"two"</span>, <span class="number">3.0</span>, <span class="string">"over."</span>)</div><div class="line">fmt.Printf(<span class="string">"Printf: %d, %s, %f, %s"</span>, <span class="number">1</span>, <span class="string">"two"</span>, <span class="number">3.0</span>, <span class="string">"over."</span>)</div><div class="line">fmt.Print(<span class="string">"======"</span>) <span class="comment">// 增加一行观察 Printf 的换行行为</span></div></pre></td></tr></table></figure>
<p>输出:</p>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div></pre></td><td class="code"><pre><div class="line">Print:1two3over.Println: 1 two 3 over.</div><div class="line">Printf: 1, two, 3.000000, over.======</div></pre></td></tr></table></figure>
<p>给三个函数都输入 5 个参数</p>
<ul>
<li><code>Print</code> 将 5 个参数的值以默认格式依次输出,每个值中间没有加分隔符,末尾也没有换行。(因为没有换行,这里特意加了一个句点 <code>.</code> 方便区分不同函数的输出)</li>
<li><code>Println</code> 同样以默认格式输出,只是增加了空格分隔不同的值,并且末尾增加了换行。</li>
<li><code>Printf</code> 的第一个参数跟其它参数有所区别,必须是格式化字符串(format specifier)。后续参数跟字符串里的格式化动词一一对应,按照动词指定的方式,格式化后填入对应的位置,再一起输出。</li>
</ul>
<p>接下来,重点就是这些结尾带 <code>f</code> 的函数里面,格式化动词的使用。为了跟格式化字符串里一般的内容区分开来,格式化动词以百分号 <code>%</code> 开头,后面接一个字母表示。有时候为了更精确地控制格式,在百分号和字母之间还会可能会有标志选项(如整型数填充前导零,浮点数控制小数点的位数)。</p>
<p>在不是特别严谨的语境,<strong>动词</strong> 可以是指由 百分号(%)、标志选项(可选)、字母 这三者组合的整体。但更严谨地说,动词特指后面的字母。理解这一点有助于读懂下面的文档。</p>
<p>下面直接 选译/注释 文档中关于格式化动词的部分:</p>
<p>(部分格式与 Go 的版本有关,这里选译的是当下最新的 1.16 版本)</p>
<hr>
<p><code>fmt</code> 包实现了格式化输入输出(I/O),其功能类似于 C 语言的 <code>printf</code> 和 <code>scanf</code> 。格式化 <strong>动词(verbs)</strong> 是从 C 语言的动词中衍生出来的,但更简单。</p>
<p><strong>动词</strong>:</p>
<h4 id="一般动词"><a href="#一般动词" class="headerlink" title="一般动词"></a>一般动词</h4><figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div></pre></td><td class="code"><pre><div class="line">%v 以默认格式输出值 (v 代表 Value,不同类型的默认格式参见下方内容)</div><div class="line"> 当打印结构体 (struct) 时,加号 (%+v) 会添加字段名</div><div class="line">%#v 输出值的 Go 语法表示</div><div class="line">%T 输出值类型的 Go 语法表示 (T 代表 Type)</div><div class="line">%% 输出一个百分号 (%);不消耗任何值 (因为 % 用作了动词开头,为了区分,输出 % 需要转义)</div></pre></td></tr></table></figure>
<blockquote>
<p>注:只看介绍,所谓输出 “ Go 的语法表示” 并不直观。实际上这是指一个值在代码里的字面量形式。</p>
<p>对于输出值和字面量一样的类型(布尔类型、数字类型),没有差别;对于字符串,“语法表示意味着带上引号;对于剩下的派生类型,意味着语法表示需要包含类型信息。</p>
<p>看几个例子:</p>
</blockquote>
<figure class="highlight go"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div><div class="line">20</div><div class="line">21</div><div class="line">22</div><div class="line">23</div><div class="line">24</div><div class="line">25</div><div class="line">26</div><div class="line">27</div><div class="line">28</div><div class="line">29</div><div class="line">30</div><div class="line">31</div><div class="line">32</div></pre></td><td class="code"><pre><div class="line">i := <span class="number">1</span></div><div class="line">fmt.Printf(<span class="string">"%v\n"</span>, i)</div><div class="line">fmt.Printf(<span class="string">"%#v\n"</span>, i)</div><div class="line">fmt.Printf(<span class="string">"%T\n"</span>, i)</div><div class="line"></div><div class="line">fmt.Println()</div><div class="line"></div><div class="line">a := <span class="keyword">struct</span> {</div><div class="line"> name <span class="keyword">string</span></div><div class="line"> age <span class="keyword">int</span></div><div class="line">}{name: <span class="string">"alice"</span>, age: <span class="number">24</span>}</div><div class="line">fmt.Printf(<span class="string">"%v\n"</span>, a)</div><div class="line">fmt.Printf(<span class="string">"%#v\n"</span>, a)</div><div class="line">fmt.Printf(<span class="string">"%T\n"</span>, a)</div><div class="line"></div><div class="line">fmt.Println()</div><div class="line"></div><div class="line"><span class="keyword">type</span> Person <span class="keyword">struct</span> {</div><div class="line"> name <span class="keyword">string</span></div><div class="line"> age <span class="keyword">int</span></div><div class="line">}</div><div class="line">p := Person{name: <span class="string">"bob"</span>, age: <span class="number">26</span>}</div><div class="line">fmt.Printf(<span class="string">"%v\n"</span>, p)</div><div class="line">fmt.Printf(<span class="string">"%#v\n"</span>, p)</div><div class="line">fmt.Printf(<span class="string">"%T\n"</span>, p)</div><div class="line"></div><div class="line">fmt.Println()</div><div class="line"></div><div class="line">s := []<span class="keyword">int</span>{<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>}</div><div class="line">fmt.Printf(<span class="string">"%v\n"</span>, s)</div><div class="line">fmt.Printf(<span class="string">"%#v\n"</span>, s)</div><div class="line">fmt.Printf(<span class="string">"%T\n"</span>, s)</div></pre></td></tr></table></figure>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div></pre></td><td class="code"><pre><div class="line">1</div><div class="line">1</div><div class="line">int</div><div class="line"></div><div class="line">{alice 24}</div><div class="line">struct { name string; age int }{name:<span class="string">"alice"</span>, age:24}</div><div class="line">struct { name string; age int }</div><div class="line"></div><div class="line">{bob 26}</div><div class="line">main.Person{name:<span class="string">"bob"</span>, age:26}</div><div class="line">main.Person</div><div class="line"></div><div class="line">[1 2 3]</div><div class="line">[]int{1, 2, 3}</div><div class="line">[]int</div></pre></td></tr></table></figure>
<h4 id="布尔类型(Boolean)"><a href="#布尔类型(Boolean)" class="headerlink" title="布尔类型(Boolean)"></a>布尔类型(Boolean)</h4><figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div></pre></td><td class="code"><pre><div class="line">%t 单词 true 或者 false (t 代表 True value,真值)</div></pre></td></tr></table></figure>
<h4 id="整型数(Integer)"><a href="#整型数(Integer)" class="headerlink" title="整型数(Integer)"></a>整型数(Integer)</h4><figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div></pre></td><td class="code"><pre><div class="line">%b 以 2 为基数输出 (b 代表 Binary,二进制)</div><div class="line">%c 输出对应 Unicode 码点所代表的字符 (c 代表 Character,字符)</div><div class="line">%d 以 10 为基数输出 (d 代表 Decimal,十进制)</div><div class="line">%o 以 8 为基数输出 (o 代表 Octal,八进制)</div><div class="line">%O 以 8 为基数输出,以 0o 为前缀 (同上,大写表示增加前缀)</div><div class="line">%q 一个单引号字符,按 Go 语法安全转义。 (q 代表 quote,引号)</div><div class="line">%x 以 16 为基数输出,a-f 为小写字母 (x 代表 heXadecimal)</div><div class="line">%X 以 16 为基数输出,A-F 为大写字母 (同上,大写表示字母大写)</div><div class="line">%U Unicode 格式:如 U+1234;与 "U+%04X" 效果一样 (U 代表 Unicode)</div></pre></td></tr></table></figure>
<blockquote>
<p>注:特别说明一下 <code>%c</code> 和 <code>%q</code>。</p>
<p>首先需要注意到,自 1.9 以后,<code>byte</code> 类型实际上是 <code>uint8</code> 的别名(alias),<code>rune</code> 则是 <code>int32</code> 的别名。</p>
<p>这意味着如果以 <code>%v</code> 输出,这两个类型都会被当做数字输出。</p>
<p>想要输出对应的字符,就要考虑使用 <code>%c</code> 。</p>
<p><code>%q</code> 也是输出字符,只是有两点区别:</p>
<ol>
<li>带单引号</li>
<li>对于不可打印字符(non-printable characters,不过叫『不可见字符』更容易理解),会按 Go 语法进行转义。</li>
</ol>
<p>举例说,对于字母 A,<code>%c</code> 输出 <code>A</code>,<code>%q</code> 输出 <code>'A'</code> ;中文也是类似效果。而对于换行符,对应一个换行的动作,而不是一个可以看得见的字符,用 <code>%c</code> 输出会得到一个换行,用 <code>%q</code> 输出则得到 <code>'\n'</code> (得到一个转义)。</p>
<p>两者的区别跟 <code>%v</code> 与 <code>%#v</code> 的区别比较类似。</p>
</blockquote>
<h4 id="浮点数和复数的(浮点数)成分"><a href="#浮点数和复数的(浮点数)成分" class="headerlink" title="浮点数和复数的(浮点数)成分"></a>浮点数和复数的(浮点数)成分</h4><p>(Floating-point and complex constituents)</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div></pre></td><td class="code"><pre><div class="line">%b 无小数科学记数法,指数以 2 为底数(但整数部分和指数部分均为十进制数),</div><div class="line"> 相当于以 strconv.FormatFloat 函数的 'b' 格式,</div><div class="line"> 如:-123456p-78 (分隔的 p 代表 power (of 2), 2 的幂)</div><div class="line">%e 科学记数法,如:-1.234456e+78 (e 代表 Exponent,指数)</div><div class="line">%E 科学记数法,如:-1.234456E+78</div><div class="line">%f 无指数的小数,如:123.456 (f 代表 floating-point,浮点数)</div><div class="line">%F %f 的同义词</div><div class="line">%g 指数较大时等同于 %e ,否则为 %f 。精度在下面讨论。</div><div class="line"> (换言之,%g 取 %e 和 %f 中较短的格式表示)</div><div class="line">%G 指数较大时等同于 %E ,否则为 %F</div><div class="line">%x 十六进制记数法(指数为十进制,底数为 2 ),如:-0x1.23abcp+20</div><div class="line"> (与 %b 的区别是,左边的实数为十六进制,而且可以有小数)</div><div class="line">%X 十六进制符号大写,如:-0X1.23ABCP+20</div></pre></td></tr></table></figure>
<blockquote>
<p>注:这部分的个别动词,在输出时可能同时混用 二进制、十进制和十六进制,记忆起来会比较混乱。如 <code>%x</code>,实数(又叫尾数)为十六进制,底数为 2,指数却又是十进制。建议大家自己在代码里实际尝试,加深印象。</p>
<p>还好如果不是涉及特殊数值的运算和表示,特殊的动词一般用得不多。日常表示浮点数,掌握 <code>%f</code>, <code>%e</code> 和 <code>%g</code> 就够了。关于浮点数的多种字面量表示方法,可以参考往期的内容 <a href="../../2020/golang-in-action-day-2/">Go 语言实战(2): 常量与变量</a> 中,浮点数字面量部分。</p>
</blockquote>
<h4 id="字符串与字节切片"><a href="#字符串与字节切片" class="headerlink" title="字符串与字节切片"></a>字符串与字节切片</h4><p>(对以下动词而言两者等价)</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div></pre></td><td class="code"><pre><div class="line">%s 字符串或字节切片未经解释的字节(uninterpreted bytes) (s 代表 String,字符串)</div><div class="line">%q 一个双引号字符串,按 Go 语法安全转义</div><div class="line">%x 以十六进制数输出,小写,每个字节对应两个字符</div><div class="line">%X 以十六进制数输出,大写,每个字节对应两个字符</div></pre></td></tr></table></figure>
<blockquote>
<p>注:想理解何为 uninterpreted,先要理解何为 interpreted。</p>
<p>对于脚本语言,解释器就叫 interpreter;分析或执行读入的内容,得到结果的过程,就是解释 interpret。如解释 <code>1 + 2</code> ,得到 <code>3</code> 。</p>
<p>在这里,对于字符串(字符序列)而言,解释主要是指字符转义。<code>%s</code> 动词不会对字符序列的内容进行转义。</p>
<p>但这里有一个非常容易让人迷惑的点,看下面例子:</p>
</blockquote>
<figure class="highlight go"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div></pre></td><td class="code"><pre><div class="line">str1 := <span class="string">"1\t2\n3"</span></div><div class="line">fmt.Printf(<span class="string">"%s\n-----\n"</span>, str1)</div><div class="line"></div><div class="line">str2 := <span class="string">`1\t2\n3`</span></div><div class="line">fmt.Printf(<span class="string">"%s\n-----\n"</span>, str2)</div><div class="line"></div><div class="line">str3 := []<span class="keyword">byte</span>{<span class="string">'1'</span>, <span class="string">'\\', '</span>t<span class="string">', '</span><span class="number">2</span><span class="string">', '</span>\\<span class="string">', '</span>n<span class="string">', '</span><span class="number">3</span><span class="string">'}</span></div><div class="line">fmt.Printf("%s\n-----\n", str3)</div></pre></td></tr></table></figure>
<p>输出</p>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div></pre></td><td class="code"><pre><div class="line">1 2</div><div class="line">3</div><div class="line">-----</div><div class="line">1\t2\n3</div><div class="line">-----</div><div class="line">1\t2\n3</div><div class="line">-----</div></pre></td></tr></table></figure>
<blockquote>
<p>第一个例子很容易让人以为 <code>%s</code> 还是发生了转义。</p>
<p>实际上转义发生在源码编译阶段,而不是输出阶段。也就是对于双引号字符串,编译器已经对其完成了转义。<code>str1</code> 储存在内存里的内容,是 [‘1’, 9, ‘2’, 10, ‘3’] ,其中 9 就是制表符的 ascii 码,10 是 换行符的 ascii 码。这里已经找不到 反斜杠、字母 t 和 n 了。</p>
<p>再看接下来的两个例子就很好理解了。反引号字符串告诉编译器不要转义,字节切片则直接逐个指定每个字节的内容,所以 <code>str2</code> 和 <code>str3</code> 的字节序列里,储存的就是字面意义的 “\t” 和 “\n” 。</p>
<p>当然还有更直观的方式,可以看出字节序列的不同:</p>
</blockquote>
<figure class="highlight go"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div></pre></td><td class="code"><pre><div class="line">str1 := <span class="string">"1\t2\n3"</span></div><div class="line">fmt.Printf(<span class="string">"% x\n"</span>, str1)</div><div class="line"></div><div class="line">str2 := <span class="string">`1\t2\n3`</span></div><div class="line">fmt.Printf(<span class="string">"% x\n"</span>, str2)</div><div class="line"></div><div class="line">str3 := []<span class="keyword">byte</span>{<span class="string">'1'</span>, <span class="string">'\\', '</span>t<span class="string">', '</span><span class="number">2</span><span class="string">', '</span>\\<span class="string">', '</span>n<span class="string">', '</span><span class="number">3</span><span class="string">'}</span></div><div class="line">fmt.Printf("% x\n", str3)</div></pre></td></tr></table></figure>
<p>输出:</p>
<p>(具体每个十六进制数对应的字符,这里就不再解释了,反正不同是非常直观的)</p>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div></pre></td><td class="code"><pre><div class="line">31 09 32 0a 33</div><div class="line">31 5c 74 32 5c 6e 33</div><div class="line">31 5c 74 32 5c 6e 33</div></pre></td></tr></table></figure>
<h4 id="切片"><a href="#切片" class="headerlink" title="切片"></a>切片</h4><figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div></pre></td><td class="code"><pre><div class="line">%p 以十六进制数表示的第 1 个元素(下标 0)的地址,以 0x 开头</div><div class="line"> (p 代表 Pointer,指针,也就是以指针形式输出地址)</div></pre></td></tr></table></figure>
<h4 id="指针"><a href="#指针" class="headerlink" title="指针"></a>指针</h4><figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div></pre></td><td class="code"><pre><div class="line">%p 十六进制数地址,以 0x 开头</div><div class="line"> %b, %d, %o, %x 和 %X 动词也可以用于指针,</div><div class="line"> 实际上就是把指针的值当作整型数一样格式化。</div></pre></td></tr></table></figure>
<h4 id="v-的默认格式"><a href="#v-的默认格式" class="headerlink" title="%v 的默认格式"></a>%v 的默认格式</h4><figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div></pre></td><td class="code"><pre><div class="line">bool: %t</div><div class="line">int, int8 等有符号整数: %d</div><div class="line">uint, uint8 等无符号整数: %d, 如果以 %#v 输出则是 %#x</div><div class="line">float32, complex64 等: %g</div><div class="line">string: %s</div><div class="line">chan: %p</div><div class="line">指针: %p</div></pre></td></tr></table></figure>
<h4 id="复合对象"><a href="#复合对象" class="headerlink" title="复合对象"></a>复合对象</h4><p>对于复合对象,将根据这些规则,递归地打印出元素,像下面这样展开:</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div></pre></td><td class="code"><pre><div class="line">struct: {field0 field1 ...}</div><div class="line">array, slice: [elem0 elem1 ...]</div><div class="line">maps: map[key1:value1 key2:value2 ...]</div><div class="line">上述类型的指针: &{}, &[], &map[]</div></pre></td></tr></table></figure>
<h4 id="宽度与精度"><a href="#宽度与精度" class="headerlink" title="宽度与精度"></a>宽度与精度</h4><p>宽度由紧接在动词前的一个可选的十进制数指定。如果没有指定,则宽度为表示数值所需的任何值。</p>
<p>精度是在(可选的)宽度之后,由一个句点(<code>.</code> ,也就是小数点)和一个十进制数指定。如果没有句点,则表示使用默认精度。如果有句点,句点后面却没有数字,则表示精度为零。例如:</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div></pre></td><td class="code"><pre><div class="line">%f 默认宽度,默认精度</div><div class="line">%9f 宽度 9,默认精度</div><div class="line">%.2f 默认宽度,精度 2</div><div class="line">%9.2f 宽度 9, 精度 2</div><div class="line">%9.f 宽度 9, 精度 0</div></pre></td></tr></table></figure>
<p>宽度和精度以 Unicode 码点为单位,也就是 runes。(这与 C 语言的 <code>printf</code> 不同,后者总是以字节为单位。)标志中的任意一个或两个都可以用字符 <code>*</code> 代替,从而使它们的值从下一个操作数获得(在要格式化的操作数之前),这个操作数的类型必须是 <code>int</code> 。</p>
<blockquote>
<p>注:<code>*</code> 的用法并不直观,举个例子就很好理解。</p>
<p><code>fmt.Printf("%*.*f", 6, 3, 4.5)</code></p>
<p>输出 <code>4.500</code> (注意 4 前面有一个并不明显的空格,加上数字和小数点,宽度正好为 6 )</p>
</blockquote>
<p>对于大多数的值来说,宽度是要输出的最小符号(rune)数,必要时用空格填充。</p>
<p>然而,对于 字符串、字节切片 和 字节数组 来说,精度限制了要格式化的输入长度(而不是输出的大小),必要时会进行截断。通常它是以符号(rune) 为单位的,但当这些类型以 <code>%x</code> 或 <code>%X</code> 格式进行格式化时,以字节(byte)为单位。</p>
<p>对于浮点值,宽度设置字段的最小宽度,精度设置小数点后的位数;但对于 <code>%g</code> / <code>%G</code>,精度设置最大的有意义数字(去掉尾部的零)。例如,给定 <code>12.345</code>,格式 <code>%6.3f</code> 打印 <code>12.345</code>,而 <code>%.3g</code> 打印 <code>12.3</code>。<code>%e</code>、<code>%f</code> 和 <code>%#g</code> 的默认精度是 6 ;对于 <code>%g</code>,默认精度是唯一识别数值所需的最少数字个数。</p>
<blockquote>
<p>注:关于如何精确控制浮点值的宽度和精度,这段说明看似说清楚了,实际执行中却常常让人迷惑。看网上的讨论,已经有很多人在诟病这一点。跟更早的文档相比,现在的版本好像已经调整过表述,但是帮助有限。</p>
<p>如果你需要精确控制以达到排版对齐一类的目的,可以参考这个讨论 <a href="https://stackoverflow.com/questions/36464068/fmt-printf-with-width-and-precision-fields-in-g-behaves-unexpectedly" target="_blank" rel="external">https://stackoverflow.com/questions/36464068/fmt-printf-with-width-and-precision-fields-in-g-behaves-unexpectedly</a></p>
<p>讨论篇幅过长且拗口,不再翻译。总的来说,精度控制有效数字,但因为有效数字不包括小数点和前导零,带前导零和小数点的数会更长;宽度控制最小宽度,在长度不足时会填充到指定宽度,但超出时并不会截断,总位数仍然可能超出。最后你可能需要制表符 <code>\t</code> 来帮助对齐。</p>
</blockquote>
<p>对于复数,宽度和精度分别应用于两个分量(均为浮点数),结果用小括号包围。所以 <code>%f</code> 应用于 <code>1.2+3.4i</code> 输出 <code>(1.200000+3.400000i)</code> 。</p>
<h4 id="其它标志"><a href="#其它标志" class="headerlink" title="其它标志"></a>其它标志</h4><figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div></pre></td><td class="code"><pre><div class="line">+ 始终输出数字值的符号(正负号);</div><div class="line"> 对 %q(%+q) 保证只输出 ASCII 码 (ASCII 码以外的内容转义)</div><div class="line">- 空格填充在右边,而不是左边</div><div class="line"># 备选格式:二进制(%#b)加前导 0b ,八进制(%#o)加前导 0 ;</div><div class="line"> 十六进制(%#x 或 %#X)加前导 0x 或 0X ;%p (%#p) 取消前导 0x ;</div><div class="line"> 对于 %q ,如果 strconv.CanBackquote 返回true,则输出一个原始(反引号)字符串;</div><div class="line"> 总是输出 %e, %E, %f, %F, %g 和 %G 的小数点;</div><div class="line"> 不删除 %g 和 %G 的尾部的零;</div><div class="line"> 对于 %U (%#U),如果该字符可打印(printable,即可见字符),则在 Unicode 码后面输出字符,</div><div class="line"> 例如 U+0078 'x'。</div><div class="line">' ' (空格) 为数字中的省略的正号留一个空格 (%d);</div><div class="line"> 以十六进制输出字符串或切片时,在字节之间插入空格 (%x, %X)</div><div class="line">0 用前导零而不是空格来填充;</div><div class="line"> 对于数字来说,这会将填充位置移到符号后面</div></pre></td></tr></table></figure>
<p>动词会忽略它不需要的标志。例如十进制没有备选格式,所以 <code>%#d</code> 和 <code>%d</code> 的行为是一样的。</p>
<p>对于每个类似 <code>Printf</code> 的函数,都有一个对应的 <code>Print</code> 函数,它不接受格式,相当于对每个操作数都应用 <code>%v</code> 。另一个变体 <code>Println</code> 在操作数之间插入空格,并在结尾追加一个换行。(注:这个我们在开头就已经讨论过)</p>
<p>无论用什么动词,如果操作数是一个接口值,则使用内部的具体值,而不是接口本身。因此:</p>
<figure class="highlight go"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div></pre></td><td class="code"><pre><div class="line"><span class="keyword">var</span> i <span class="keyword">interface</span>{} = <span class="number">23</span></div><div class="line">fmt.Printf(<span class="string">"%v\n"</span>, i)</div></pre></td></tr></table></figure>
<p>会输出 <code>23</code> 。</p>
<p>除了使用动词 <code>%T</code> 和 <code>%p</code> 输出时,对于实现特定接口的操作数,需要考虑特殊格式化。以下规则按应用顺序排列:</p>
<ol>
<li><p>如果操作数是 <code>reflect.Value</code>,则操作数被它所持有的具体值所代替,然后继续按下一条规则输出。</p>
</li>
<li><p>如果操作数实现了 <code>Formatter</code> 接口,则会被调用。在这种情况下,动词和标志的解释由该实现控制。</p>
</li>
<li><p>如果 <code>%v</code> 动词与 <code>#</code> 标志 (<code>%#v</code>) 一起使用,并且操作数实现了 <code>GoStringer</code> 接口,则该接口将被调用。</p>
</li>
</ol>
<p>如果格式 (注意 <code>Println</code> 等函数隐含 <code>%v</code> 动词)对字符串有效 (<code>%s</code>, <code>%q</code>, <code>%v</code>, <code>%x</code>, <code>%X</code>),则适用以下两条规则:</p>
<ol>
<li>如果操作数实现了 <code>error</code> 接口,将调用 <code>Error</code> 方法将对象转换为字符串,然后按照动词(如果有的话)的要求进行格式化。</li>
<li>如果操作数实现了 <code>String() string</code> 方法,则调用该方法将对象转换为字符串,然后按照动词(如果有的话)的要求进行格式化。</li>
</ol>
<p>对于复合操作数,如 切片 和 结构体,格式递归地应用于每个操作数的元素,而不是把操作数当作一个整体。因此,<code>%q</code> 将引用字符串切片中的每个元素,而 <code>%6.2f</code> 将控制浮点数组中每个元素的格式。</p>
<p>然而,当以适用于字符串的动词(<code>%s</code>, <code>%q</code>, <code>%x</code>, <code>%X</code>),输出一个字节切片时,它将被视为一个字符串,作为一个单独的个体。</p>
<p>为了避免在以下情况出现递归死循环:</p>
<figure class="highlight go"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div></pre></td><td class="code"><pre><div class="line"><span class="keyword">type</span> X <span class="keyword">string</span></div><div class="line"><span class="function"><span class="keyword">func</span> <span class="params">(x X)</span> <span class="title">String</span><span class="params">()</span> <span class="title">string</span></span> { <span class="keyword">return</span> Sprintf(<span class="string">"<%s>"</span>, x) }</div></pre></td></tr></table></figure>
<p>在触发递归之前先转换类型:</p>
<figure class="highlight go"><table><tr><td class="gutter"><pre><div class="line">1</div></pre></td><td class="code"><pre><div class="line"><span class="function"><span class="keyword">func</span> <span class="params">(x X)</span> <span class="title">String</span><span class="params">()</span> <span class="title">string</span></span> { <span class="keyword">return</span> Sprintf(<span class="string">"<%s>"</span>, <span class="keyword">string</span>(x)) }</div></pre></td></tr></table></figure>
<p>无限递归也可以由自引用的数据结构触发,例如一个包含自己作为元素的切片,然后该类型还要有一个 <code>String</code> 方法。然而,这种异常的情况是非常罕见的,所以 <code>fmt</code> 包并没有对这种情况进行保护。</p>
<p>在输出一个结构体时,<code>fmt</code> 不能,也不会,对未导出字段调用 <code>Error</code> 或 <code>String</code> 等格式化方法。</p>
<h4 id="显式参数索引"><a href="#显式参数索引" class="headerlink" title="显式参数索引"></a>显式参数索引</h4><p>在 <code>Printf</code>, <code>Sprintf</code> 和 <code>Fprintf</code> 中,默认的行为是,每个格式化动词对调用中传递的连续参数进行格式化。然而,紧接在动词前的符号 <code>[n]</code> 表示第 n 个单一索引参数将被格式化。在宽度或精度的 <code>*</code> 前同样的记号,表示选择对应参数索引的值。在处理完括号内的表达式 <code>[n]</code> 后,除非另有指示,否则后续的动词将依次使用 n+1、n+2等参数。</p>
<p>举例:</p>
<figure class="highlight go"><table><tr><td class="gutter"><pre><div class="line">1</div></pre></td><td class="code"><pre><div class="line">fmt.Sprintf(<span class="string">"%[2]d %[1]d\n"</span>, <span class="number">11</span>, <span class="number">22</span>)</div></pre></td></tr></table></figure>
<p>将输出 <code>22 11</code> 。 而</p>
<figure class="highlight go"><table><tr><td class="gutter"><pre><div class="line">1</div></pre></td><td class="code"><pre><div class="line">fmt.Sprintf(<span class="string">"%[3]*.[2]*[1]f"</span>, <span class="number">12.0</span>, <span class="number">2</span>, <span class="number">6</span>)</div></pre></td></tr></table></figure>
<p>等价于</p>
<figure class="highlight go"><table><tr><td class="gutter"><pre><div class="line">1</div></pre></td><td class="code"><pre><div class="line">fmt.Sprintf(<span class="string">"%6.2f"</span>, <span class="number">12.0</span>)</div></pre></td></tr></table></figure>
<p>将输出 <code>12.00</code> (注意 12 前有一个空格)。</p>
<p>因为显式索引会影响后续的动词,所以这个记号可以通过重置索引为第一个参数,达到重复的目的,来多次打印相同的数值:</p>
<figure class="highlight go"><table><tr><td class="gutter"><pre><div class="line">1</div></pre></td><td class="code"><pre><div class="line">fmt.Sprintf(<span class="string">"%d %d %#[1]x %#x"</span>, <span class="number">16</span>, <span class="number">17</span>)</div></pre></td></tr></table></figure>
<p>将输出 <code>16 17 0x10 0x11</code> 。</p>
<h4 id="格式错误"><a href="#格式错误" class="headerlink" title="格式错误"></a>格式错误</h4><p>如果给一个动词提供了无效的参数,比如给 <code>%d</code> 提供了一个字符串,生成的字符串将包含对问题的描述,像以下这些例子:</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div></pre></td><td class="code"><pre><div class="line">Wrong type or unknown verb: %!verb(type=value)</div><div class="line"> Printf("%d", "hi"): %!d(string=hi)</div><div class="line">Too many arguments: %!(EXTRA type=value)</div><div class="line"> Printf("hi", "guys"): hi%!(EXTRA string=guys)</div><div class="line">Too few arguments: %!verb(MISSING)</div><div class="line"> Printf("hi%d"): hi%!d(MISSING)</div><div class="line">Non-int for width or precision: %!(BADWIDTH) or %!(BADPREC)</div><div class="line"> Printf("%*s", 4.5, "hi"): %!(BADWIDTH)hi</div><div class="line"> Printf("%.*s", 4.5, "hi"): %!(BADPREC)hi</div><div class="line">Invalid or invalid use of argument index: %!(BADINDEX)</div><div class="line"> Printf("%*[2]d", 7): %!d(BADINDEX)</div><div class="line"> Printf("%.[2]d", 7): %!d(BADINDEX)</div></pre></td></tr></table></figure>
<p>所有的错误都以字符串 <code>%!</code> 开头,有时后面跟着一个字符(动词),最后以括号内的描述结尾。</p>
<p>如果一个 <code>Error</code> 或 <code>String</code> 方法在被输出例程调用时触发了 panic ,那么 <code>fmt</code> 包会重新格式化来自 panic 的错误消息,并在其上注明它是通过 <code>fmt</code> 包发出的。例如,如果一个 <code>String</code> 方法调用 <code>panic("bad")</code> ,则产生的格式化消息看起来会是这样的</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div></pre></td><td class="code"><pre><div class="line">%!s(PANIC=bad)</div></pre></td></tr></table></figure>
<p><code>%!s</code> 只是显示失败发生时使用的打印动词。然而,如果 panic 是由 <code>Error</code> 或 <code>String</code> 方法的 nil 接收者(receiver)引起的,则输出的是未修饰的字符串 <code><nil></code>。</p>
<hr>
<p>实际上,这一套函数的命名规则和格式化动词,基本继承自 C 语言,只是做了少量的调整和改进。有 C/C++ 经验的朋友应该非常熟悉。没有写过 C 的朋友,经过整理,也会有助于记忆和理解。</p>
<p>上述内容涉及到类型方面的知识,如果有朋友还不熟悉,可以参考往期的内容:<a href="../../2020/golang-in-action-day-3/">Go 语言实战(3): 类型</a></p>
<h4 id="Errorf"><a href="#Errorf" class="headerlink" title="Errorf()"></a>Errorf()</h4><p>Go 在 1.13 中专门为 <code>fmt.Errorf()</code> 新增了一个动词 <code>%w</code> 。文档是这样介绍的:</p>
<blockquote>
<p>如果格式化字符串包含一个 <code>%w</code> 动词,并且该动词对应一个 <code>error</code> 操作数,<code>Errorf</code> 返回的 <code>error</code> 将实现一个 <code>Unwrap</code> 方法,会返回前面传入的 <code>error</code> 。包含一个以上的 <code>%w</code> 动词 或 提供一个没有实现 <code>error</code> 接口的操作数是无效的。无效的 <code>%w</code> 动词是 <code>%v</code> 的同义词。</p>
</blockquote>
<p>文档的说明严谨但拗口。好在这部分代码不长,直接贴出来看看:</p>
<figure class="highlight go"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div><div class="line">20</div><div class="line">21</div><div class="line">22</div><div class="line">23</div><div class="line">24</div><div class="line">25</div><div class="line">26</div><div class="line">27</div><div class="line">28</div><div class="line">29</div></pre></td><td class="code"><pre><div class="line"><span class="comment">// go/src/fmt/errors.go</span></div><div class="line"></div><div class="line"><span class="function"><span class="keyword">func</span> <span class="title">Errorf</span><span class="params">(format <span class="keyword">string</span>, a ...<span class="keyword">interface</span>{})</span> <span class="title">error</span></span> {</div><div class="line"> p := newPrinter()</div><div class="line"> p.wrapErrs = <span class="literal">true</span></div><div class="line"> p.doPrintf(format, a)</div><div class="line"> s := <span class="keyword">string</span>(p.buf)</div><div class="line"> <span class="keyword">var</span> err error</div><div class="line"> <span class="keyword">if</span> p.wrappedErr == <span class="literal">nil</span> {</div><div class="line"> err = errors.New(s)</div><div class="line"> } <span class="keyword">else</span> {</div><div class="line"> err = &wrapError{s, p.wrappedErr}</div><div class="line"> }</div><div class="line"> p.free()</div><div class="line"> <span class="keyword">return</span> err</div><div class="line">}</div><div class="line"></div><div class="line"><span class="keyword">type</span> wrapError <span class="keyword">struct</span> {</div><div class="line"> msg <span class="keyword">string</span></div><div class="line"> err error</div><div class="line">}</div><div class="line"></div><div class="line"><span class="function"><span class="keyword">func</span> <span class="params">(e *wrapError)</span> <span class="title">Error</span><span class="params">()</span> <span class="title">string</span></span> {</div><div class="line"> <span class="keyword">return</span> e.msg</div><div class="line">}</div><div class="line"></div><div class="line"><span class="function"><span class="keyword">func</span> <span class="params">(e *wrapError)</span> <span class="title">Unwrap</span><span class="params">()</span> <span class="title">error</span></span> {</div><div class="line"> <span class="keyword">return</span> e.err</div><div class="line">}</div></pre></td></tr></table></figure>
<p>传入的参数,实际上通过 <code>p.doPrintf</code> (一系列 <code>Printf</code> 函数的内部实现) 变成了字符串 <code>s</code>。此时 <code>%w</code> 是 <code>%v</code> 的同义词,参数里即使有 <code>error</code> ,也是取 <code>Error()</code> 方法返回的字符串。</p>
<p>然后再看是否有需要包裹(wrap)的 <code>error</code> 。这需要一个 <code>%w</code> 动词并对应的操作数满足 <code>error</code> 接口,仅有其中之一,或者参数顺序不对应,都不算。如无,则通过 <code>errors.New(s)</code> 返回一个只有字符串的最基本的 <code>error</code> ;否则返回一个同时包含 格式化字符串 和 内部错误的 <code>wrapError</code> 。跟基本的 <code>error</code> 相比,它多了一个获取内部错误的 <code>Unwrap</code> 方法。</p>
<h3 id="输入-Scanning"><a href="#输入-Scanning" class="headerlink" title="输入 Scanning"></a>输入 Scanning</h3><p>除了输出(Printing),<code>fmt</code> 包还提供了一系列类似的函数负责输入,将特定格式的文本(formated text)解析为对应的值。</p>
<p>与 Printing 类似,通过前后缀的组合来区分读取的来源和格式化方式:</p>
<ul>
<li>前缀:<code>Fscan</code> 表示从文件(<code>io.Reader</code>)读取;<code>Scan</code> (无前缀)表示从标准输入 <code>os.Stdin</code> 读取;<code>Sscan</code> 表示从字符串读取;</li>
<li>后缀:<code>Scan</code> (无后缀)表示把换行当成普通空白字符,遇到换行不停止;<code>Scanln</code> 表示遇到换行或者 <code>EOF</code> 停止;<code>Scanf</code> 表示根据格式化字符串里的动词控制读取。</li>
</ul>
<p>Scanning 使用几乎一样的一系列动词(除了没有 <code>%p</code>, <code>%T</code> 动词,没有 <code>#</code> 和 <code>+</code> 标志),这里不再重复介绍这些动词。动词的含义也基本一致,只是在非常细微的地方,为方便输入做了变通:</p>
<ul>
<li>对于浮点数和复数,所有有效动词都是等价的;进制以文本内容、而不是动词为准。(因为尾数和指数可能是不同的进制,无法单靠动词指定)</li>
<li>对于整型数,则以动词指定的进制为准;仅在 <code>%v</code> 时依靠前缀判断进制。</li>
<li>宽度仍然有效,用来限制读取的最大符号数(去掉前导空格);如 <code>123456</code> ,如果用 <code>%3d%d</code> 来解析,会被理解为 <code>123</code> 和 <code>456</code> 两个数;精度不再有意义。</li>
<li>对于数字类型,数字之间可以添加下划线提高可读性,读取时会忽略下划线,不影响解析。</li>
</ul>
<p>其它更细致的差别(包括与 C 语言的差别),像符号的消耗,空白字符串的匹配,就不再展开。建议大家自己尝试,遇到问题直接去看文档。</p>
<h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ul>
<li><a href="https://pkg.go.dev/fmt" target="_blank" rel="external">https://pkg.go.dev/fmt</a> :<code>fmt</code> 官方文档,翻译整理难免有理解偏差,以文档为准</li>
</ul>
<hr>
<p><img src="https://i.creativecommons.org/l/by-nc-sa/4.0/88x31.png" alt="知识共享 “署名-非商业性使用-相同方式共享” 4.0 (CC BY-NC-SA 4.0)”许可协议"><br>本文为本人原创,采用<a href="http://creativecommons.org/licenses/by-nc-sa/4.0/" target="_blank" rel="external">知识共享 “署名-非商业性使用-相同方式共享” 4.0 (CC BY-NC-SA 4.0)”许可协议</a>进行许可。<br>本作品可自由复制、传播及基于本作品进行演绎创作。如有以上需要,请留言告知,在文章开头明显位置加上署名(Jayce Chant)、原链接及许可协议信息,并明确指出修改(如有),不得用于商业用途。谢谢合作。<br>请点击查看<a href="http://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh" target="_blank" rel="external">协议</a>的中文摘要。</p>
]]></content>
<summary type="html">
<p>这期算是 《<a href="../../categories/Go-语言实战/">Go 语言实战</a> 》的番外,内容以翻译整理为主。</p>
</summary>
<category term="Go 语言实战" scheme="https://jaycechant.info/categories/Go-%E8%AF%AD%E8%A8%80%E5%AE%9E%E6%88%98/"/>
<category term="golang" scheme="https://jaycechant.info/tags/golang/"/>
</entry>
<entry>
<title>现在写,可能都来不及了...</title>
<link href="https://jaycechant.info/2021/time-is-up/"/>
<id>https://jaycechant.info/2021/time-is-up/</id>
<published>2021-01-31T12:28:20.000Z</published>
<updated>2021-01-31T14:47:43.222Z</updated>
<content type="html"><![CDATA[<p>之前答读者提问,整理了《<a href="../../2021/insurance-for-kids/">为什么不要先给小孩子买保险?</a>》。</p>
<p>我本来也比较关心保险话题,顺着读者的提问和讨论,计划接着聊聊穗岁康和重疾新规相关的话题。</p>
<p>如果你留意了我的推送,就会发现,最终还是没能好好聊。眼下这个时间点,可能已经不及了。</p>
<p>随便敲两个字,算是交代一下这个话题。</p>
<a id="more"></a>
<h2 id="穗岁康"><a href="#穗岁康" class="headerlink" title="穗岁康"></a>穗岁康</h2><p>作为一个外行,虽然都是个人看法,强调仅供参考,也想好好查查资料好好写,也是给自己做调研。</p>
<p>结果一忙,拖到去年年底。眼看穗岁康第一批就要截止,差一天,同样的保费却少了一个月的保障期。仓促写了一下<a href="../../suisuikang/">穗岁康</a>,并提醒有需要的朋友赶紧投保。</p>
<p>作为一个文笔不行的强迫症,写完排完版都到了 12 月 31 日晚上,如果当晚大家没有看到,推送的意义就大打折扣了,所以用了一个比较抢眼球的标题:《还剩不到两小时,错过这批白白损失一个月》。</p>
<p>到这里为止,只能说抢眼球,还不能说标题党——如果大家看完文章,确实能买到穗岁康的话。结果推送完我自己试了一下,发现医保系统调整,早在下午就关闭了购买入口。换言之,文章还没发出,第一批就买不到了。<strong>标题党实锤了</strong>。</p>
<p>朋友调侃我标题党,我也无从辩解,只好苦笑着认了。</p>
<p>心里计划着,后面好好写一下重疾新规。到时顺便提醒大家,对穗岁康有需求的人群,少一个月保障期也还是可以考虑买的。</p>
<p>然后到今天,一个月又过去了。</p>
<p>我去试了一下,穗岁康居然没有像上次那样提前关闭,不知道文章推送时还有没有。感兴趣的读者朋友,可以点进去<a href="../../suisuikang/">穗岁康</a>的文章看看,决定要不要买。就是要赶紧。</p>
<h2 id="重疾新规-与-择优理赔"><a href="#重疾新规-与-择优理赔" class="headerlink" title="重疾新规 与 择优理赔"></a>重疾新规 与 择优理赔</h2><p>然后是<strong>重疾新规</strong>。</p>
<p>时间关系,不展开说,反正这些内容,给定了关键词,文章一搜一大把。</p>
<p>简单说,2020 年 11 月 5 日,保险行业协会发布了新的重疾险规范。新规范在 2021 年 2 月 1 日实施,不影响在这之前已经成立的保单。旧定义的保险最晚要在 1 月 31 日下架。已经有很多旧定义产品提前下架了。所以说,这个醒,本来早该提。</p>
<p>新规范完善了很多细节定义,变得更细致也更科学了。多数调整是对投保人有利的,例如增加了 3 种重疾,例如明确了很多疾病的定义,减少了保险公司自行解释的余地。但也有『不利』的,最明显的就是原本直接按重疾赔付的甲状腺癌,现在按照分期,可能按轻症赔付。</p>
<p>长远来说,这是好事,会促进重疾险的健康发展。哪怕是看起来『不利』的调整,也是在平衡保险公司和投保人的关系。要知道,现在甲状腺癌甚至被称作喜癌:早期发现的甲状腺癌可能花不了多少钱就能治愈,还能按重疾赔一大笔钱。</p>
<p>按照旧定义,保司当然只能赔。为了减少理赔,很多产品只好用更严的健康告知,阻挡那些有潜在甲状腺癌风险的人投保。于是,有甲状腺结节的人群,可能因此而失去其它重疾的保障。要知道,随着超声技术的发展,普通人群里甲状腺结节的发现率已经在 4% 以上,近年可能还一直在攀升。</p>
<p>但是,视角转换到个人。我们不去考虑整个行业和整个社会,单从个人的利益最大化出发,旧定义和新定义,怎么选?</p>
<p>这时不得不提<strong>择优理赔</strong>。</p>
<p>如果光比较新旧定义,可以说孰优孰劣,没有定论,要具体案例具体分析。尤其新定义的产品,上市得还不多,特别是未来究竟是降价还是涨价,也说不定,带来了很多的不确定性。</p>
<p>保险公司可能看出了大家的犹豫,于是纷纷提出『择优理赔』,促销了一波。</p>
<p>简单说,如果买了旧定义的保险,同时支持择优理赔,将来理赔时,投保人可以在新旧定义中,选择对自己有利的定义进行理赔。</p>
<p>在未来新产品尚不明确的时候,先把保单确定下来,同时可以择优理赔,确实是对投保人的福利。</p>
<p>何况还有犹豫期。如果先买了旧定义产品,将来在犹豫期内看到更好的新定义产品,完全可以退保。而如果将来发现新产品都不如旧产品,就没有后悔药可以吃了。</p>
<hr>
<p>可是这文章写得太晚了。这个点,又是最后两小时,不知道还有多少产品还没下架?还没下架的产品,不知道业务员还扛不扛得住涌进的保单,能够赶在下架前投保?</p>
<p>何况还有健康告知的问题。择优理赔看的是重疾定义,但是健康告知是不会择优的。如果不符合投保时的健康告知,无论如何都不会理赔的。如果早半个月,发现有健康告知的疑点,还能去医院做个检查,给结节分个级。</p>
<p>所以现在这个时间点,如果你<strong>非常明确自己需要重疾险</strong>,而且也<strong>认同旧定义+择优理赔是个保底的选择</strong>,并且身体<strong>没有不符健康告知</strong>的毛病,如果还<strong>有合适的产品</strong>没有下架,买吧。然后犹豫期内再好好考虑考虑。</p>
<p>但这么多个如果,估计很难都符合,那就不要赶这趟车了。便宜总不能都让你占了,等后续新产品出来慢慢看吧。</p>
<h2 id="题外话"><a href="#题外话" class="headerlink" title="题外话"></a>题外话</h2><p>我们家在 16 年底买的预售房,本应 18 年底收楼。按计划应该简单收拾精装的房子,置点家具电器,通风个半年,在 19 年下半年入住。</p>
<p>可因为房子的质量问题,我们拒收了房子,跟开发商扯皮了一年多。直到 20 年的 315 晚会(你懂的),开发商才作出了部分让步。期间为了保留证据,房子一直空置。直到19 年下半年,实在觉得不能等下去了,才妥协签字收楼,并开始装修,决定自行修复房子的问题。</p>
<p>这中间经历生娃、经历了疫情,装修进程不得不多次中断。</p>
<p>不得不说,装修真是这世界上少有的同时费脑、费钱、费时间、还得出体力的活动。特别是我们这样带『精装修』的,想省钱,不得不小心翼翼保护原有装修的。特别是没有经验,一开始师傅说啥就是啥的。</p>
<p>到最后收尾时回顾,发现投入了那么多心血,却换来处处遗憾。</p>
<p>孩子逐渐长大,租住的房子越来越捉襟见肘。我现在连一张书桌都无处安放,只能在厅的一堆杂物旁,就着落地支架用笔记本。眼看马上就要入住新房,无论花时间换租,还是花钱买新家具改善租住环境,都显得不再划算。</p>
<p>一堆事情环环相扣,变成了时间和空间上的大型华容道与九连环。</p>
<p>眼下最快速能够腾出时间和空间的,显然就是赶紧了结这已经拖了太久的装修。</p>
<p>所以过去的一个月里,我疯狂地缺啥买啥,不断地约各种安装师傅,尽量在新年放假前赶进度。简单的安装我甚至都自己上阵。</p>
<p>去年过年前,原本说不回家过年的工头有事回了老家。等到小区重新对外开放,工头做完别处的单回来继续,已经是 20 年下半年。</p>
<p>现在仿佛又在重现。所以我一有空就跑新房,一直忙到晚饭时间。躺下之前要看着孩子睡着,然后躺在床上继续下单需要的东西,直到睏到睁不开眼。</p>
<hr>
<p>一不小心发了这么长的牢骚。其实就是想狡辩一下,我为什么这么长时间不发文章。</p>
<p>装修真是一个不断地接受不完美的过程,特别是市面上有那么多二把刀的师傅。而那些手艺好的师傅,审美却又迷一般守旧顽固。</p>
<p>有机会也许能聊聊装修。给不了什么好的建议,起码可以躲开我们踩过的坑。</p>
<hr>
<p><img src="https://i.creativecommons.org/l/by-nc-sa/4.0/88x31.png" alt="知识共享 “署名-非商业性使用-相同方式共享” 4.0 (CC BY-NC-SA 4.0)”许可协议"><br>本文为本人原创,采用<a href="http://creativecommons.org/licenses/by-nc-sa/4.0/" target="_blank" rel="external">知识共享 “署名-非商业性使用-相同方式共享” 4.0 (CC BY-NC-SA 4.0)”许可协议</a>进行许可。<br>本作品可自由复制、传播及基于本作品进行演绎创作。如有以上需要,请留言告知,在文章开头明显位置加上署名(Jayce Chant)、原链接及许可协议信息,并明确指出修改(如有),不得用于商业用途。谢谢合作。<br>请点击查看<a href="http://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh" target="_blank" rel="external">协议</a>的中文摘要。</p>
]]></content>
<summary type="html">
<p>之前答读者提问,整理了《<a href="../../2021/insurance-for-kids/">为什么不要先给小孩子买保险?</a>》。</p>
<p>我本来也比较关心保险话题,顺着读者的提问和讨论,计划接着聊聊穗岁康和重疾新规相关的话题。</p>
<p>如果你留意了我的推送,就会发现,最终还是没能好好聊。眼下这个时间点,可能已经不及了。</p>
<p>随便敲两个字,算是交代一下这个话题。</p>
</summary>
</entry>
<entry>
<title>Go 语言实战(8):命令行(3)CLI 框架</title>
<link href="https://jaycechant.info/2020/golang-in-action-day-8/"/>
<id>https://jaycechant.info/2020/golang-in-action-day-8/</id>
<published>2020-12-30T15:55:36.000Z</published>
<updated>2021-02-21T16:29:38.013Z</updated>
<content type="html"><![CDATA[<p>经过前面两期的介绍,相信大家已经可以写简单的命令行程序,并且能够使用命令行参数。</p>
<p>即使遇到一些困难,建立直观认识和了解关键词之后,在网络上搜索答案也变得相对容易。</p>
<p>接下来介绍 CLI 框架。</p>
<a id="more"></a>
<p>命令行程序的前两期:</p>
<ul>
<li><a href="../golang-in-action-day-6/">命令行程序(1)</a> </li>
<li><a href="../golang-in-action-day-7/">命令行程序(2)</a> </li>
</ul>
<p>本系列完整目录:</p>
<p><a href="../../categories/Go-语言实战/">Go 语言实战系列</a></p>