-
Notifications
You must be signed in to change notification settings - Fork 0
/
Go tutorial
2391 lines (2176 loc) · 210 KB
/
Go tutorial
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
以下内容均是从golang官方提供的入门教程中总结的: https://tour.golang.org/welcome/1
Packages
1.每个Go程序都是由包(s)组成的
2.Go程序从main包(的main函数)开始运行
3.使用import关键字导入包
4.包名和导入路径的最后一个元素名称相同
*.从当前看,包就是一个文件夹,里面有好多文件,并且这些文件的第一个有效行都是"package 包名"
*.import关键字导入包名应该隐式的以Go安装路径下的src文件夹为默认搜索路径,这个是我猜的,但应该没问题,因为从导入的包名看"fmt", "math/rand", fmt是src下的文件夹, math是src下的文件夹,rand是math下的文件夹,所以说拿安装路径下的src文件夹作为基路径或说搜索路径应该没啥问题
Imports
1.在使用import关键字导入包时,可以使用多个import,每个import后跟一个包名,也可以使用一个import,将所有要导入的包都放在括号里。官方推荐使用第二种方法,官方叫它"因式分解法"
*.pa+pb+pc=p(a+b+c)
import "fmt"
import "math"
import ("fmt";"math")
import (
"fmt"
"math"
) //通过最后两个import判断"因式分解法"中多个包的分隔符为";"或"\n"
*.上面星号的最后一个import的写法是官方的写法,倒数第二个如果多个包写在一行要用分号隔开
Exported names
1.使用import将包导入进来以后,程序只能引用包中以大写字母开头的定义,可以理解为当执行import的时候会将相应包中以大写字母开头的定义给导进当前程序的空间,而那些以非大写字母开头的定义仅仅可以在包内部引用,官方将这些以大写字母开头的叫做"exported name",从字面意思上理解是"导出的名称",此时这个"导出"的动作应该是看作在执行"import 包名"的时候,会将包里以大写字母开头的定义从"包"里导出到程序的应用空间中。这么解释可能不太对,但目前这么解释的话是有帮助理解的。
2.一个包是由同一目录下一个或多个首行相同(package X)且文件扩展名为 .go 的文件组成,而在import的时候应该是将包里所有文件中的以大写字母开头的定义都导到程序空间中了。这个也是猜的,不过通过Packages那一节的例子中的math.Int //到这断了,应该是误删了,后面应该是 "这个例子可以推断出来"
*.先来简单的理解一下module和package, 准确来说,module是一个目录, 而package是一个或多个具有相同首行,"package Name",的文件集合. 而为了好维护, 通常将具有相同包名的文件放在同一个目录下(而使用import导入包时,找的恰巧就是这个目录), 而且包通常会被作为某个功能导入到其它包内,所以一般来说, 一个文件夹中只能包含一个包(注意是一个包而不是一个源文件),如果在同一个目录下有多个包,则在使用import导入的时候会出错, 原因是import后面跟的路径的最后一个元素是包所在目录的名字(通常会将包名和包所在目录名命名为同一个,假设有一个模块叫Animals,它下面有个文件夹叫sheep,在sheep文件夹中有一个包,package sheep,则我们在使用import "Animals/sheep"导入后,引用时,直接使用sheep.X就行,但如果sheep文件夹中的包不叫sheep,而是pig,则在使用import "Animal/sheep"后你还要查一下这个包名到底是啥才能进行引用,这就是为什么通常我们在设计目录时,都会将包名与其所在目录名设置为同一个的原因, 如果目录名和包名不同,引用的时候还不想查包名到底是啥,可以指定一个别名,然后使用这个别名对包中exported进行引用,格式为 alias import "moduleName/dir/of/package"),如果你在这个目录下有多个包,程序不知道你到底要导入的是哪一个. 当然,包所在目录还可以有子目录, 如有一个模块A,其下有一个目录叫A1,A1下有包A1(即源文件的头一行为package A1),此时我们要使用A1这个包,则直接使用import "A/A1",此时在A1目录下还可以有A11,在A11下同样可以有包,如这个包就叫A11,此时我想引用A11这个包,则可以直接使用import "A/A1/A11", 如果要问为啥不将A11与A1设置为同一级,那样看着也没这么繁索,这就是设计问题了, 可以是可以,只不过具体问题具体分析时,可能这样设计看起来会更合理一些.
Functions
1.函数可以带任意多个参数,包含0个
2.函数参数中,形参类型出现在形参的后面
3.函数基本语法func func_name(arg1 type1, arg2 type2) return_type { statements } //这块要注意函数如果有形参,则形参的类型一定不能少,即func f(a)是错误的,要写成func f(a type).
Functions continued
1.当函数中连续的参数类型相同时,可以简写,只保存最后一个参数的类型
*. x int, y int -> x,y int
Multiple results
1.如果返回多个结果可以设置多个返回值的类型,多个类型使用圆括号括起来,位置在包含函数参数的圆括号后面
2.如果函数有多个返回值,则可以直接使用返回结果进行分解赋值
Named return value
1.函数返回值也可以被命名,就是函数定义中返回类型的位置,之前讲返回多个值的时候该位置是括号里放上返回类型,现在只是在返回类型前再加上变量名,作为返回的变量;如果要返回这种带有名称的返回值时,return语句后不接任何参数(这次看又发现了问题,return是不用加参数,但函数体中的变量名必须用函数声明时在返回值类型中所定义的变量,感觉这种情况只是在编译阶段编译器自动将返回值类型中定义的名称给加到函数中了,因为函数变量的赋值要不使用关键字var,要不使用海象赋值符:=,但给定的例子中直接使用的等号=进行赋值,就说明编译器在执行到x,y赋值之有就已经声明了,而这个声明应该是编译器阶段做的)。
2.需要注意的是这种形式最好只在较短的函数中用,如果函数较长的话用这种形式会损失程序可读性。
3.带名称的返回值在命名时最好是有意义的,即根据返回值名称即可知道返回的大概是什么东西
Variables
1.使用var来定义单个变量或是多个变量,变量的形式和函数参数中的形式差不多
*. var a int
var a,b,c int
var ( //这是变量声明的因式分解形式
a int = 3
b string = "张三"
)
2.var关键字可以用在包级别,也可用在函数级别,说白了就是既可在函数里面使用var定义变量,也可在函数外面使用var定义变量。可能不准确但就目前看到的可以先这样讲
Variables with initializers
1.声明变量时可以包含初始化器,一个变量一个;所谓的初始化说中了就是变量赋值,这句话说白了就是声明变量时可以给变量赋值,一个变量一个值,这样更明了一些
*.var i, j int = 1, 2
2.如果声明变量时有初始化器,则变量后面的类型可以忽略,程序会将初始化器的类型作为变量的类型。换句话说就是声明变量时可以不指明变量类型,编译器会根据变量值进行推断补全,简单来说就是定义一个变量var a = 1, 然后编译的时候,编译器发现1是整型,然后将这个声明变量的语句补全为var a int = 1.
Short variable declarations
1.变量声明不仅可以通过var关键字,还可以使用:=符号,使用这种形式不需要var关键字,而且不用也不能显式的给出变量类型。但是:=和var有一点不同的是,var既可用在包级别又可用在函数级别,而:=只可用在函数级别
2.对照1来看,包级别的语句都需要以关键字打头
3.既然:=不需要显示给出变量类型,则肯定是用到推断功能了(infer)
Basic types
1.Go中的基本类型有:
布尔型: bool
字符串: string
(有符号)整型: int int8 int16 int32 int64 //除int外,取值范围-2^(N-1)到2^(N-1)-1
无符号整型: uint uint8 uint16 uint32 uint64 uintptr //除unit和uintptr外,取值范围0到2^N-1
*.在理解这个取值范围时,如不进行计算,可以从两方面合起来去理解这个范围:第一,这个类型总共可以存储多少个数; 第二,最小数起的数是哪个.如int8可以理解为可以存储2^8个数,因为是有符号的,所以第1位表示符号,剩下7位才表示的是数字,所以最小数是-2^7=-128,从这个最小数往上数呗,够2^8个就是最大数了,即127,其实正数最大应该是+2^7=128,但0还算一位, 所以最大到127.
*.int uint uintptr默认是自适应的,即虽然看起来int有好几种,uint也有好好种,但如果你是在32位系统上运行时,编译器会动将int精确为int32,也就是说我们用的时候只需要使用int和uint就行,不用人为的去写后面的位数是32还是64; 但是如果你明确知道你想存储的是16位的,则你就用int16,这样即使你在32或64位系统上运行,用的也是int16,这样可以减少内存的占用. 至于uintptr还不会用,它的作用见:Https://stackoverflow.com/questions/59042646/whats-the-difference-between-uint-and-uintptr-in-golang
字节型: byte //uint8的别名
文字型: rune //int32的别名,表示一个Unicode码点
*.rune通常作为单个字符的类型,且单个字符需要使用单引号引起来,var char rune = 'b'或var char rune = '杨', 得到的结果是unicode数值,使用string(char)可以再次得到相应的单个字符.举例来说, for _,r := range "Hello, 世界!" {这里的r就是rune类型或说成是int32类型,因为rune是int32的别名,而非字符串类型}
符点型: float32 float64 //int 和float的最大值最小值在src/math/const.go中都有说明,也备注了算法,也可以直接使用math.MaxInt32等这样的形式直接输出
复合型: complex64 complex128 //complex64用来描述float32类型的实数和虚数,complex128用来描述float64类型的实数和虚数,详述:https://www.cloudhadoop.com/2018/12/golang-tutorials-complex-types-numbers.html
2.变量的声明也可以使用"因式分解"式,即使用一个var关键字完成多个变量的声明
3.int,uint和uintptr这三个类型在32位系统中它们的位宽是32位,在64位系统下它们的位宽是64位
4.对于整型一般就用int,除非定义时知道数值的大小int可能不够再去考虑其它整型(废话) //这不是废话,上面在"无符号整型"后的备注中提到了,我们写的是int,但实际执行的时候,编译器是会根据程序所在系统的位数去相应的分配空间,而这句话存在的意义我能想到的一种情景是: 假设我们用的系统是32位的系统,如果直接使用int的话,编译器预处理替换为int32,但是我们想要存的数要比int32大,此时我们就可以显式的指定数值类型为int64,此时编译器在处理这块的时候,会自动为我们分配64bit的内存空间去存放这个数,就不会去考虑当前系统是32位的事儿了.
Zero values
1.当声明变量时,如果没有设置初始化器,则变量会默认赋一个该变量所属类型的零值:
数值类型: 0
布尔型: false
字符串: ""
Type conversions
1.Go中的类型转换必须要精确指定要转换到的类型
,表现在实际的写法上即要将被转换值作为函数名为目标类型的函数参数
var i int = 42
var f float64 = float64(i)
var u uint = uint(f)
还可以使用:=形式:
i := 42
f := float64(i)
u := uint(f) //对于数值和字符串相转换,如var a int = 97 , var b string = string(a) 这种写法得到的b是小写字母"a",而不是我们想要的"97", 想要得到"97"要使用strconv包的Itoa函数, var b string = strconv.Itoa(a), 得到b为"97" (https://yourbasic.org/golang/convert-int-to-string/) Itoa is short for "Integer To Ascii"
Type inference
1.到目前为止变量声明的几种形式:
var a int //默认会为a赋值0
var a int = 1 //带有初始化器的形式
var a = 1 //有初始化器时变量类型可省,自动根据初始化器进行推断
var a, b int //同时声明多个类型相同的变量
var a,b int = 1, 2 //同时声明多个类型相同的变量且带有初始化器的
var a,b = 1, 2 //同时声明多个类型相同的变量,且带有初始化器,此时变量的类型可忽略,会自动初始化器进行判断
a := 3 //:=这种赋值形式本来对类型就是隐式的需要根据值进行判断,并且不需要关键字var;但只能用于函数级别
2.如果之前定义了变量i, var i int,则将i作为一个新变量的值时,j := i , 新变量的类型和i的类型相同
3.如果使用:=声明的变量类型为数值型常量时,该变量的类型要看数值的精度,从而确定是int, float64, complex128还是...
Constants
1.常量声明使用关键字const
2.常量值可以是字符(单引号,一个字符,rune或int32类型),字符串(双引号,一个或多个字符,string类型),布尔值或是数值
*.for _, r := range "hello, 世界!" {这里r是单个字符,所以是rune或int32类型,而非字符串类型}
3.常量不能使用:=语法声明
4.这个是自己猜的,常量定义时就必须赋值,否则会出错,程序不会给默认值 //仔细想想这么做也是有道理的,常量代表固定值,或说后期不可改的值,即然这样,你声明常量时如果不初始化,让编译器给个zero value就没啥意义了,所以干脆就设计成常量声量时必须有值,没值编译不过.
Numeric Constants
1.常量声明也可使用因式分解法"factored" //还可以使用因式分解法的有import和var
2.一个未明确写明类型的常量具体的类型要根据右边的值来确定
3.位运算符: & | ^ << >> &:都有1才是1; |:有一个是1就是1; ^:有且只有一边是1才是1; <<:左移变大; >>:右移变小 //该教程中举的例子很好:https://www.tutorialspoint.com/go/go_bitwise_operators.htm
*.理解左移右移的技巧,把当前数值的二进制放在一个左右严丝合缝的卡板中间,这两个卡板位置是不变的,然后对整体进行左移或右移,移动之后,由于卡板位置没变,肯定会在左边或右边空出位置,在空出的位置进行补零即可.举例来说,1,二进制是00000001,我们把它放到卡边里|00000001|,比如说左移3位,则把卡边中的数字整体左移3位,变成了|00001***|,由于卡板不变,右边对应多出3个空位,此时对空位补0,变成|00001000|,这就是1左移3位得到的结果.右移同理,只不过是在左边补零.
*****
var (
a = 1 //00000001
b = 2 //00000010
c = a & b //00000000
d = a | b //00000011
e = a ^ b //00000011
f = a >> b //|00000001|->整体右移2位->|**000000|->补零->|00000000|
g = a << b //|00000001|->整体左移2位->|000001**|->补零->|00000100|
)
fmt.Println(a) //a = 1
fmt.Println(b) //b = 2
fmt.Println(c) //c = 0
fmt.Println(d) //d = 3
fmt.Println(e) //e = 3
fmt.Println(f) //f = 0
fmt.Println(g) //g = 4
*****
For
1.Go中只有一种循环结构,for循环
2.for循环的基本结构为for关键字,然后是由两个分号分隔的三部分,接着是由一对花括号将要执行的语句括起来。需要注意的是由分号分隔的那三部分不需要使用圆括号括起来
3.init部分的变量声明仅在该for循环中有效
*.这个for的每一部分就不具体说了,基本上和其它语言的for循环没区别
For continued
1.这个是接上一节的For讲的,主要说的是由分号分隔的那三部分中的第一部分init和第三部分post都是可选的,即如果不写也是正确的。其实如果省略第一和第三部分,则一般情况下,第一部分会从for循环外部语句获取,第三部分会在for循环语句中获取,总的来说for循环的三件套还是完整的,只是出现的位置不一样罢了
2.虽然省略了第一和第三部分,但是那两个分号还是结构中必须有的 //这句话是错误的,即只有条件时, 两边的分号也是可以省略的, 也面会讲到
*.抓住for循环的本质,有控制节奏的变量,有含有变量的判断条件,有变量的改变控制何时判断条件结束. // for initial; condition;post
For is Go's "while"
1.上面一节还说了如果省略了初始化部分和post部分但两个分号还是必须有的,这节就打脸了,即使省略了第一三部分,两个分号也可省略,这样写下来看起来就像其它语言中的while循环了,但还是那句话,虽说省略,但那三个基本点肯定在上下文都 是可以找到的
*. func main() {
sum := 1 //这部分相当于init
for sum < 1000 {
sum += sum //这部分充当了post
}
}
Forever
1.这节还是继续对for结构由分号分隔的三部分做说明,这次是条件部分也可省略,如果这部分省略,如果在执行语句中没有设定退出条件,则会永远的执行下去,话又说回来了,如果在执行语句中设定退出条件,这不是又是换汤不换药嘛,只不过相应的元素没有位于由分号分隔的位置;抓本质!
func main() {
sum := 1
for{
if sum < 1000{
fmt.Println(sum)
sum ++
} else {
break
}
}
} //上面的语句中随然for循环部分啥都没有,但其实三要素都在,initializer为sum:=1,条件部分为if{}else{},第三部分为sum++; 其实大家都在
*.对于for循环中的那两个分号,语法要点就是,要么两个都在,要么一个都不能在.
If
1.Go中最基本的if语句也是if引导,后面跟条件表达式,注意这块和for相似,条件部分不需要使用圆括号括起(对于for是由两个分号隔开的三部分不需要使用圆括号括起,但是无论是if还是for那个位置都是一样的,都是位于关键字后,起始花括号前);然后后面就是跟着由一对花括号括括起来的执行语句
If with a short statement
1.这节讲的if的特性目前接触的其它语言里没有,或是没有见过,就是在if的条件表达式前加一个语句,类似于for中的init部分,仅仅在最开始时执行一次
2.if中的init部分和条件表达式部分也是使用分号隔开
3.if语句中的init部分声明的变量的作用范围为它所在的if语句,生命周期为if语句执行结束
*. if v := math.Pow(x, n); v < lim {
return v
}
If and else
1.if语句中最最常用的还有一个else关键字,这节主要讲的是if结构中init如果定义了变量,则不仅在if的语句块中有效,在相应的else语句块中也有效
*.if v := math.Pow(x,n); v < lim {
return v
} else {
fmt.Printf("%g >= %g\n", v, lim)
}
Exercise: Loops and Functions
1.练习做了,主要学到的倒不是循环和函数的用法, 而是求平方根的方法,牛盾法, 大概的方法是使用公式z-=(z^2-x)/(2z), 使用for循环,循环执行该语句, 直到z值不变或是变动很小时停止,则最后这个值就是我们求得的平方根结果.其中x是被求平方根的值,z通常是1.0, x或x/2.
Switch
1.Switch语句其实是if...else...的更短的一种书写形式,当if...else...过多的时候使用switch语句可读性会好一些; switch同if一样,也可以有initializer部分,同条件部分用分号隔开,当然也可以省略initializer部分,具体用不用根据实际情况去定; 使用switch时,当所有条件都不满足时,执行的是default关键字下的语句,这点要注意.
2.在像C等一些语言中也有switch语句,在这些语句中,每个case下的执行语句都要有一个break来跳过继续对下面case检测执行。但在Go中不需要在每条case下的执行语句中都加上break语句,可以认为Go已经为我们考虑好了,在程序编译阶段自动为我们在每个case下补上了break语句,所以说本质上还是有跳出switch语句的逻辑.
3.Go中case后面的类型不限于变量,且相应case部分也不一定要是整数. 从官文给出的例子来看,常见的形式有三种:
switch下是变量名,case下是变量名所匹配的值; switch var {case 1: xxx; case 2: xxx; default: xxx} //为了写在一行,不同case用分号隔开,但实际能否这样用还没试过
switch下有初始值;以及用来匹配case的变量; switch var := expression; var {case 1: xxx; case 2: xxx; default: xxx}
switch下是空,case下是值为true或false的逻辑表达式; switch {case a>b: xxx; case a<b: xxx; default: xxx}
switch后跟逻辑表达式,case值为true或false; switch 3>2 {case true:xxx;case false: xxx}
*.switch主要用于替代较长的if...else...
Switch evaluation order
1.这节主要讲的是如果switch要有多个case时,这些case的评估顺序是啥样的,这个其实都知道,是从上向下的,并且一旦遇到哪个case后面的值匹配成功了,则执行完相应case下面的语句后就不再继续向下匹配其它的case后面的值了,因为之前也说过,Go自动为每个case下面的执行语句的最后面加了break来跳出switch
Switch with no condition
1.当switch中无条件表达式时,条件表达式相当于true
2.其实又回到要理解问题的本质上来了,其实所谓的无所件,只是条件部分不在switch的条件表达式部分了,而是转移到每个case值上,万变不离其宗
*.当switch无条件时,主要是处理较长的if...then...else...
*.switch {
case t.Hour() < 12:
fmt.Println("Good morning!")
case t.Hour() < 17:
fmt.Println("Good afternoon!")
default:
fmt.Println("Good evening.")
}
Defer
1.如果在函数调用前加上defer关键字的话,在执行到带有defer前缀的函数时,仅仅会将该函数进行预处理,即将变量换成值这类的操作,但并不会立刻执行,直到这个函数的外层函数执行完毕后,才会开始执行该函数
*.需要注意的是预处理的仅仅是deferred函数定义的参数部分, 函数体中的变量还是在执行的时候才会替换的,看例子:
-----
func DeferStack(){
i := 0
defer func(x int){ fmt.Println(i, x)}(i) //执行时,只有最后括号里的这个(i)被替换了,即预处理后变成了func(x int){fmt.Println(i,x)}(x=0),而函数体中的语句是在函数真正执行的时候才进行处理的.
i+=1
defer func(x int){ fmt.Println(i, x)}(i)
i+=1
defer func(x int){ fmt.Println(i, x)}(i)
fmt.Println(i)
}
-----
Stacking defers
1.如果函数中存在多个defer时,则这些被defer的函数会按照代码的执行顺序依次入栈,而栈的原理是后进先出,所以在相应的外层函数执行完后,里面带有derer的函数是被从后向前执行的
*.从上面DeferStack函数的例子结果中也可看出, DeferStack函数在执行完最后一条语句后并没有退出, 而是等待着里面被defer修饰的子函数都执行完毕后才退出的, 不然也不会出现结果中三个defer函数中i的值都是2.
Pointers
1.Go语言也支持指针,关于指针的概念之前经常在C和C++上听说
2.Go中的指针比较简单只有一种声明形式,不支持运算
*T: var p *int //声明一个指针p,p中存的是地址,该地址指向的内存区域存的是int型值(话说内存地址有类型吗?查了一下好像是有,是无符号整型,表示数据存储在内存中的起始位置)
3.给指针变量赋值的一般形式是: 地址符&加上变量名
i := 42 p = &i
4.指针变量前加上*表示的是该指针变量指向内存区域的值
*p = 21 //指针变量存放的实际是内存地址,就该例来说p存放的实际上是在内存中为变量i分配的地址;而加上*之后,*p表示的是指针变量p的值(变量i的内存地址)指向的值). 此时*p其实就是i,所以*p=21就可以理解为i=21.
*.对于指针的理解,感觉还要从对变最的理解讲起. 把每个变量后都加一个词,"指向",感觉更易于理解.如
var i = 42
var p *int
p = &i
fmt.Println(i) //输出的是变量i的指向,或说成输出的是i地址处存的值
fmt.Println(p) //输出的是变量p的指向,或说成输出的是p地址处存的值
fmt.Println(*p)//输出的是变量p指向的指向,或说成输出的是p地址处存的值(即i的地址)所指向的值,也就等价于第一个输出中所说的: 输出的是i地址处存的值.
**.实际编译时,会将i,p,*p先替换为相应等价的地址, 然后输出该地址所存放的值,这句话感觉是理解变量与指针的关键.
Structs
1.tutorial中讲的是struct(结构体)是一个字段的集合,这个可以理解,在C中看过对struct类型的描述,C中其实也是一堆字段的集合,而且这些字段的类型可以是任意的,就目前可以简单的理解成struct体中是一堆没有initializer的变量声明(可能不准确,但目前看是这样子的)
Struct Fields
1.对struct中的field(字段,成员)的访问,是通过点符号(".")实现的
type Vertex struct {
X int
Y int
}
func main() {
v := Vertex{1, 2}
v.X = 4
fmt.Println(v.X)
}
*.不同语言中对于同一概念的翻译可能不同,最重要的还是要明白其本质.
Pointers to structs
1.还可以通过struct pointer(结构体指针)来访问struct fields(结构体的字段或叫成员)
2.当p的类型为struct pointer时,正常情况下应该使用(*p).X才能够访问结构体中的field X,但是Go语言允许我们在不明确表示废弃*前缀的情况下直接使用p.X的方式来访问结构体中的X field(好多初学者其实对于一些概念的描述可能会很迷糊,因为以前没接触过,脑子里没有可供参考的东西,我当时也是,现在再回头看,就要简单的多;比如这点开头说的p的类型为结构体指针,要明白,其实说的就是p中存放的值指向的内存地址处存放的数据其类型为结构体,而给结构体指针p前加上*号前缀就表示该结构体内容本身了.举例来说,有10个抽屉,相当于内存中的地址,我将一把锁放在了第3个抽屉,那么 假设声明了var p *锁,则p的值就是3, *p就是那把锁)
3.struct value加上&符号,返回的是一个指向struct value的指针(这句话听起来也别扭,首先struct value是个什么东西?(从例子上看其实就是类似于类的实例,这么说不准确,准确来说应该就是类的实例,因为结构体也是一种类型,所谓类型就是类,所以说struct value其实就是类的实例)也不好描述,看例子吧: p=&Vertex{1,2} 这个例子中,Vertex{1,2}相当于描述中讲的struct value; 此时p是一个指针变量,也就是说p中存放的是Vertex{1,2}这个实例在内存中被分配的地址,然后上一节也说了按理说如果要引用Vertex中的值的话,需要用(*p).X/Y,但是Go语言在这块为我们做了处理,即p的*前缀可以省略掉)
Struct Literals
1.struct literal,看完例子感觉,所谓的Struct Literals是指通过给结构体类型中字段赋值的方式来得到stucture value,也就是说可以将Structure Literal理解为一种得到structrue value的手段.而学到的新东西是在实例化化的时候,结构体中的所有字段不必完全都赋到值,可以只赋一部分,剩余那部分没赋到的,会根据之前讲过的,按相应类型的"零值"去赋(整型的是0,字符串类型的是""等等吧,这个也有一部分是自己猜的,不过应该没啥大问题),下面是官文中给的例子:
type Vertex struct { X, Y int }
var (
v1 = Vertex{1, 2} // has type Vertex
v2 = Vertex{X: 1} // Y:0 is implicit
v3 = Vertex{} // X:0 and Y:0
p = &Vertex{1, 2} // has type *Vertex
)
*.这是该小节的第一句话, A struct literal denotes(表示) a newly(新) allocated struct value by(通过) listing(列出/登记) the values of its fields.
2.我感觉把Struct Literal叫做结构体的实例化方法或叫创建结构体的对象的方法比较合适(根据给出的例子推想出来的)
Arrays
1.数组类型的声明: [n]T //一个含有n个元素类型为T的数组(声明一个数组,该数组包含n个类型为T的元素)
var a [2]string //声明一个数组a,该数组中有2个类型为string的元素
2.数组的引用和赋值与其它语言一样,都是使用索引的形式
a[0] = "Hello"
a[1] = "World"
3.声明数组时如果限定了长度,则以后使用该数组时,长度就是固定的了,不可以对数组的长度进行重定义;但最后一句话不是很明白:This seems limiting, but don't worry; Go provides a convenient way of working with arrays.它说的意思是明白,前面说了声明时带有长度的数组后期不可改变大小,但又说这种限制不用担心,Go为我们提供了一种方便的方式来使用数组,这句话指的是什么?指的是后面要说到了声明时可以不定义长度的形式吗?
4.声明数组时赋值(或者叫声明一个带有初始化器的数组?) [n]T{value1, value2, ...}
var primes := [6]int{2, 3, 5, 7, 11, 13} //这里看出错误了,因为这里的声明用的是短形式:=,所以说打头的关键字var是没有必要并且一定不能有的,看了一眼原文也是这样,先不去掉var,暂且留在这里吧
5.总结一下数组的声明赋值常用方法:
a := [2]int{1,2} //短形式的海象符必须在声明时就赋值
var a [2]int a[0]=1 a[1]=2 //使用var可以先声明,后面使用索引的方式赋值
var a [2]int = [2]int{1,2} //使用var同样可以连声明和赋值在一块做了
var a = [2]int{1,2} //使用var可以省略左边声明部分的显式类型说明,因为可以根据右边推断出来
6.数组的长度必须是一个固定的数值,不可以是动态的,因为Go是一门静态类型的语言, 所以必须要在编译时知道其长度.如果想声明一个大小不固定的"数组",可以考虑使用下面要说的分片,slice. //https://stackoverflow.com/questions/46809499/why-cant-you-put-a-variable-as-a-multidimensional-array-size-in-go
7.官文有这么一句话: "An array's length is part of its type, so arrays cannot be resized",所以当别人问你var animals [3]string中,变量animals的类型时,回答是[3]string, 而非[]string,或string.
Slices
1.首先说了array的大小是固定的。接着引出相对而言slice是变化的,slice在其它语言中经常翻译为分片,通过看他的描述,在Go里对slice翻译为分片也是合适的。而分片其实指的是取array的一部分,也就是说slice一般是和array联用,更准确一点说slice的underlying是array;
2.slice也有类型,为[]T,即未指定大小,[]中未指定分片的大小,也就是说大小是动态的,T一般为相应array中所包含元素的类型;
3.分片通常的用法是先声明一个数组,然后通过指定数组的上限和下限来为slice类型变量赋值(或说得到一个slice类型对象)
*. primes := [6]int{2, 3, 5, 7, 11, 13}
var s []int = primes[1:4] //这里面的[]int是可以省略的,也可写作:
s := primes[1:4]
4.接着3说,数组的分片一般是包前不包后的,即a[1:4]是指数组a中索引1到索引3这三个元素,并不包含索引4指向的元素.
*.第三条的例子,可以看出分片s的类型是[]T,这个现在就死记吧,数组的分片(感觉叫切片更生动一些)(不知道将slice和array连起来说准不准确,暂且先这样记吧),一开始我以为[]T只是因为方便不用刻意的去关心分片的大小,事实上Go可能也是这样考虑的;我尝试着将分片s的类型换成[3]int,因为后面的分片就三个元素嘛,但是执行失败了。而且回显明确告诉我分片的类型是[]T(不能指定slice包含元素的数量)
Slices are like references to arrays
1.slice不存储任何数据,对slice中任意元素的更改其实是直接改变该slice所对应的underlying array.举例来说 underlying array为base:=[10]int{1,2,3,4,5,6,7,8,9,10}这10个数,然后我对该underlying array做了一个切片,比如slice:=base[0:5],我们都知道变量声明完之后其实是在内存中分配了一片空间,然后使用变量时,其实是利用变量的地址准确的找到变量数据在内存中的位置(大概是这样,具体的也记不清了),再说这里的分片,其实也是内存中的一段地址,特殊的是这个地址是在underlying array的范围内取,这样一来,其实对分片数据的更改,就直接作用到underlying array中的数据。一开始我想用array的别名来描述slice,但是这样不准确,因为别名是对underlying array的完全引用而slice可以是整个基础数组也可能是基础数组的一部分,所以不能把slice理解成其underlying array的别名
2.既然slice会直接改变它underlying array中的数据,则引用同一个array的其它slice可能也会受到影响,具体受不受影响就看两个slice在引用同一个array时有没有取到相同的索引.
Slice literals
1.前面有个struct literal从例子中看,struct literal就是一个创建结构体实例或是对象的方法或叫手段,即通过给结构体中字段进行赋值的方法来得到struture value(也就是所谓的结构体实例或对象)
2.对slice literal官方tour给的描述是:"a slice就像是一个没有长度的array literal", 参考struct literal就很好理解了,其实总结起来就是:
0.涉及两个概念 T literal和T value
1. T literal是得到T value的一种手段或方法;
2. T literal的具体表现形式为: "T{val1,...}";
*.拿结构体和本节的slice举例来说:
假设有结构体type Person struct{ Age int },则Person{18}就是一个Person value,它是通过给结构体定义中的字段赋一个实际值得到的,也就是通过所谓的Person literal方式.
再说slice, []int{1,2,3}这是一个slice value, 它是通过实际给出slice元素值来得到的, 也就是所谓的slice literal,这种通过给出实际类型值的方式或手段就叫做T literal,得到的结果就叫做T value
3.声明一个array literal: [3]bool{true, true, false}
4.声明一个slice literal: []bool{true, true, false}
*.其中3和4的区别是,3只是简单的声明一个含有三个元素类型为bool的array并赋值;而4其实可以理解为2步,第一步是创建一个无名array第二步是基于第一步的无名array创建一个大小和无名array完全一样的slice, array[:].
5.对于元素类型为结构体的slice,在声明时初始化需要注意的是,不能像对单个结构体初始化一样,只给部分field赋值,剩下的field自动获得相应类型的初始值; 必须要将结构体中所有的field都赋值,否则会报错.
s := []struct {
i int
b bool
}{
{2, true},
{3, false},
{5, true},
{7, true},
{11, false},
{13, true},
} //这个例子中还有需要注意的地方呢,当初始化array或slice时,如果结尾花括号,},和最后一个元素在同一行,则最后一个元素后的逗号可加可不加, 但如果像该例中这么写,即最后一个元素和结尾花括号不在一行,则最后一个元素后一定要加逗号,不然会报错.
*.上面这个例子中slice元素实际上是简写的, 这样想啊, 元素其实是某个类型的实例, 而结构体的实例化语法正常来说应该是T{val...},而这里省了T,直接写的是{val...},拿第一个元素举例,完整的写法应该是: struct{i int; b bool}{2, true},需要注意的是这里面struct"{i int; b bool}"才是类型(字段间我用分号隔开的, 因为这样可以写在一行, 也可以用常规写法, 即每个字段占一行), 而不是"struct",之所以这样说可以抽象一下,slice实例化的语法为[]T{val...},也就是说右方框号,],到左花括号,{,中间的部分的所有内容表示的是元素类型.
6.总结一下:参照structure literal, 所谓的slice literal就是使用[]T{x,...}的方式得到slice value的一种方法
7.再多说一句,从之前的struct literal,到array literal, 再到slice literal,其实头两个基本上就是之前讲的变量声明的时候就顺带赋值了,而slice literal虽说也是声明的时候就赋值了,但是不要忘了,slice是要基于array的,这句话是关键。所以在理解slice literal的时候就要分两步理解,第一步是array的创建,第二步是slice的build(叫构建,生成啥的,随便叫吧)
8.slice也是有类型的,它的类型是"没有长度的数组类型" []T
9.再说一下slice literal的官方定义:slice literal就像是一个没有长度的array literal
Slice defaults
1.如果slice的low bound和或high bound省略的话,则默认会用它们的默认值替代,即,如果low bound省略则用0替代;如果high bound省略的话,则用len(array)替代
var a [10]int
a[0:10]
a[:10] //low bound default to 0
a[0:] // high bound default to 10
a[:] //low bound default to 0 and high 10
2.看这节例子结果的时候我其实一开始有点理解跑偏了,我把所有的操作都和数组联系起来,其实不是的:
s := []int{2, 3, 5, 7, 11, 13} //这个s是对右侧无名数组的一个分片
s = s[1:4] //这个是从上面那句得到的s中取第1,2,3个元素(3,5,7)
s = s[:2] //这个是在上面那句s的结果取第0和第1个值(3,5)
s = s[1:] //这个是在上面那句的s的结果取第1个值(5)
*.上面几节也说过slice其实是对array的引用,我理解slice其实存的是地址,而不是值,对slice[1]的修改,其实是对相对应的array在内存中的数据进行修改,所以才会有之前说的对一个slice的修改,可能会影响到其它的slice,如果它们有相同引用的话. //现在想来, 首先这句话说的没问题, 但又有不恰当的地方,其实所有的变量都是对地址的引用, 只不过slice不同的是系统没有为其分配新内存,而是局限在给underlying array分配的内存范围. 下面的知识会提到如果追加的内容超出了underlying array的范围会重新给underlying array分配一块足够的地方, 但slice本身还是会被局限在这块重新给underlying array分配的内存上,本质是没变的.
*.上面那4个s,第2,3,4分别是以上一句为源进行截取,但是下一节会讲到,它们是可扩展的,对于:
s = s[1:4]来说, s的长度最大扩展到5, 即 s = s[:5], 因为cap(s[1:4])=5;
s = s[:2]来说, s的长度最大扩展到5,即 s = s[:5], 因为cap(s[:2])=5;
s = s[1:]来说, s的长度最大扩展到4,即 s = s[:4],因为cap(s[1:])=4;
上面这三个例子还可以换种角度去理解, 拿最后一个s = s[1:]来说, 你要弄清这里的最低索引1在基础数组中的索引是几,比如是2, 然后用基础数组的长度L-2,就是此时s的capacity, 分片少的话可以使用这种方法,分片多的话还是上面说的这种方式合理, 即基于上一个分片的情况进行计算.
*.所以弄清slice扩展的原则就是:对于sliceA = sliceB[[low]:[high]]来说,要明确知道:
a.cap(sliceA)是多少;
b.sliceA的内容是啥;
此时sliceA索引的最小值总是为0.
Slice length and capacity
1.每个slice都有两个属性,一个叫length,一个叫capacity.
length的值为当前slice中含有的元素的个数
capacity的值为当前slice所基于的那个array的长度减去当前slice的low bound
*.这么说其实不严谨,当看到create slice with make那一节的例子时, 你会发现,当一个slice是基于另一个slice得到的, 而非基于underlying array时,当前slice的capacity就等于所基于的那个slice的capacity减去当前slice的最小索引.所以对于capacity个人感觉定义为"capacity的值为当前slice所基于的那个array或slice的capacity减去当前slice的low bound"更严谨. //这么想其实是没问题的, 总的来说slice还是基于underlying array, 只不过, 如果使用某个slice1进行分片而不是underlying array时, 得到的slice2由于low bound的索引总是从0开始, 所以你在计算slice2的capacity时,就不得不用slice1的capacity去计算. 这句注释如果后面看不明白,可以在纸上写个简单的例子看一下应该就理解了,给个例子吧, 下面这个例子,注释中的数字个数表示len,数字个数加上星号个数表示cap:
=====
func TestSlice() {
s := []float64{2, 3, 5, 7, 11, 13} // 2 3 5 7 11 13
fmt.Println(s, len(s), cap(s)) //6 6
s = s[1:4] // 3 5 7 * *
fmt.Println(s, len(s), cap(s)) //3 5
s = s[:2] // 3 5 * * *
fmt.Println(s, len(s), cap(s)) //2 5
s = s[1:] // 5 * * *
fmt.Println(s, len(s), cap(s)) //1 4
}
=====
2.可以通过len(s)来得到分片s的length属性值
可以通过cap(s)来得到分片s的cap(s)属性值 //看到这明白了goroutines 有一节的代码中的cap()函数是啥意思了
*.len(s)理解为当前slice的长度; cap(s)理解为当前slice可能的最大长度
*.len()函数没啥说的,好理解; 而cap()函数我觉得应该理解为:"左值不动, 只能右值向右扩展",此时再去理解cap()的值是len(array)-slice(low bound)就通了
3.capacity其实可以理解为当前分片最多可以拥有的元素个数,这个个数不要和当前分片的长度相混淆,当前分片的长度指的是当前分片已有元素的个数,而当前分片最多可以拥有元素的个数倾向于分片的扩展性,而这个扩展性是看你当前是基于谁进行的分片,如cap(slice)的值为4,则slice2:=slice[这里的长度不能超过4],因为此时slice2的内容是基于slice来的,所以要注意不是说分片参考的就一定是underlying array, 还有可能参考的是另一个分片(但本质上基于的还是underlying array).
4.对于所谓的扩展slice的长度,要从以下几点考虑:
a.最原始的那个slice或是array,因为此时length和capacity相等且最大;
b.你要扩展的是谁,因为它决定了capacity的大小, 你只有知道了你所基于的这个slice或array的capacity才能知道你基于此得到的新slice的capacity最大是多少
originalSlice := []int{1,2,3,4,5} //原始slice的length和capacity相等为5
firstSlice := originalSlice[2:4] //此时firstSlice的length为4-2=2;capacity为原始slice减去low,即5-2=3;所以下面如果想要以firstSlice进行扩展,最大长度不能超过3
resultSlice := firstSlice[:3] //此时high索引最大只能到3,因为你是根据firstSlice这个slice取的, 而firstSlice的capacity为3.
*.由cap()函数引出的问题, 什么时候会用到这个函数呢? 首先说用到该函数, 其实用的是它的特性, 那么它的特性是啥呢,即可以知道当前slice可容纳的最大元素个数,结合len()函数,就可以知道当前slice可扩展的空间有多大(或者理解为当前slice还剩多少空间). 虽说下面会讲到当使用append()函数向slice追加元素时, 当追加的元素个数超出了slice的capacity后,会重新给underlying array分配一块地儿,但重新分配后,内存地址就改变了. 具体码代码时, 可以根据这段话所描述的特性去使用cap()函数.
Nil slices
1.之前讲过如果变量声明的时候(使用关键字var,不能是海象符,使用海象符时一定要进行赋值操作)如果没有初始化器,也就是说如果声明时不赋值,则Go会按类型给相应的变量赋一个值,这个值Go叫它zero value,也就是零值。不管叫啥明白这个意思就好。同样的slice如果声明时不赋值也会默认有一个zero value,为nil.
2.从例子来看,所谓slice声明时不赋值,指的是没有右边的数组那部分,var s []int,这样的slice,它的len()和cap()都是0,因为这个slice没有underlying array(这个词我还是觉得翻译为基础数组好一些)
3.一般是用if语句去判断slice是否未初始化.这块就要注意了, "var a []int" 和 "var a = []int{}" 是不一样的,前者是没有初始化,此时a值为nil,而后者的值不是nil, 另外通过实践还从编译器得到一句话,"slice can only be compared to nil",即在使用if语句判断的时候,条件部分的"=="号一边是slice名称,另一边只能是nil.//得到提示使用的例子为:var x []int = []int{}; if x==[]int{}{ fmt.Println("not nil!")}
Creating a slice with make
1.首先Go提供了一个内建的函数,make,它的返回值就是一个slice;文中还有一句"this is how you create dynamically-sized arrays"也就是make是用来创建动态数组的方式,记得其它语言中当数据的类型为[]T时声明的就是动态数组,但是在Go中,[]T只是slice的类型,数组array的类型必须是[n]T即必须指定大小。还接着上面那句英文,感觉给的例子不是创建的动态数组呀,"a := make([]int, 5)"还是指定了数组的大小了,哎,这块有点不明所以。 //现在感觉呀,golang中所谓的动态数组和C中的还不太一样, golang中的动态数组的动态是有一个大前提的,即在underlying array下的动态, 这么想应该就通了
2.内建函数make的参数可以是两个,也可以是三个,但是第一个一般都是slice的类型:
两个参数 make([]T, slice_len)
三个参数 make([]T, slice_len, slice_caps)
*.两个参数的make逻辑: 先根据[]T中的T和slice_len生成一个无名的数组,并且给该数组赋slice_len个0,此时数组生成了,再根据[]T来生成slice并返回(感觉如果没有caps参数的话,默认caps的值等于len的值)
*.三个参数的make逻辑: 先根据slice_caps确定无名数组的长度,然后根据[]T和slice_len创建slice并确定slice的元素个数(说白了就是slice中有几个0)
*.上面这两条完全是猜测,待后期接触了关于make的api说明再好好理解一翻
*.刚又悟出了两点:
1.两个参数时,省略的第三个参数值和第二个值相同;且第三个参数值表示的是underylying array的长度,第二个参数表示的是slice的长度,只不过此时slice的长度等于underlying array(此时的capacity最大)的长度;
2.三个参数可以参照两个参数,第三个参数为underylying array的长度,第二个参数为slice的长度
3.到现在是看出来了,分片的可伸缩性,只要是在underlying array所表达的范围内可以随便折腾; 还有一点需要注意,make函数中,第三个参数slice_caps一定是大于等于第二个参数slice_len的, 因为是根据第三个参数确定基础数据的长度, 而第二个参数确定的是slice的长度, 而slice是跑不出capacity手心儿的.
4.再通过例子来了解一下make两个参数和三个参数(下面这个例子举的好!):
a = make([]int, 6) 此时虽说是两个参数,但是预处理的时候第3个参数同第2个,即,在执行前原表达式会被改为: a = make([]int,6,6),然后先根据第三个参数创建一个无名数组,这个无名数组有6个0,然后根据第1,2个参数,确定slice的大小 a := [0:6].此时根据上面的算法,len(a)值为6,cap(a)的值为6-0=6.
再来说三个参数的,参照2个参数的逻辑: 假设有a = make([]int,6,8),先根据第三个参数8,创建一个含有8个int类型元素的无名数组对象(该无名数组初始元素为相应类型零值,即此时创建了一个含有8个0的无名数组), 然后根据第1,2个参数创建一个长度为6个元素的slice, 默认该slice从无名数组第0个索引处开始取,所以结果就是len(a)的值为6, cap(a)的值为8.
*.当slice中元素类型为无名struct时,搞清在声明时赋值以及先声明后赋值时写法上的区别:
声明时赋值: slice1 := []struct{Name string; Age int}{{"张三",10},{"李四",20}}
先声明后赋值: slice2 := make([]struct{Name string; Age int},2,4)
slice2[0] = struct{Name string; Age int}{"李四",20}
*.在slice1中每个分片元素的类型都是一个结构体,所以在initializer中每个元素都单独使用了花括号括起来;而slice2[0]是给单个结构体元素赋值,所以后面花括号里面直接就是字段值.
*.length指的是slice的当前实际长度, cap指的是基于被分片的slice当前slice可能的最大长度 //之前把把capacity翻译为"容量",有时候翻译为"潜力"可能有助于理解,即一个slice的length可能是3,但它有6个长度的"潜力"
Slices of slices
1.叫slices of slices,其实就像c或java中的多维数组,使用方法一模一样,都是通过多维索引,看第一眼的时候觉得会难一些,其实不然。
board := [][]string{
[]string{"-", "-", "-"}
[]string{"-", "-", "-"}
[]string{"-", "-", "-"}
} //每个元素前的"[]string"也可以省略, 想一下当结构体作为元素类型时,声明并初始化的时候,类型可以省略,直接写{X,Y}; 但是如果是单独给slice中的某个元素赋值时,非primitive类型都需要把类型名加上. 初始化并赋值时,具体元素位置加不加元素类型都可以的原因类似于最开始讲的,无论是包的导入,还是变量的声明时说过的"factored",即因式分解,说白了就是把相同部分提到最前面了. 而单独给slice中的某个元素赋值的时候必须加上元素类型是因为你直接写个"{X,Y}",Go不会对里面的元素类型做推断,也就不知道具体类型,所以此时要加.
2.strings包里的Join()函数可以将slice或是array中的字符串进行拼接并输出
Appending to a slice
1.这个很好理解,和python的列表追加函数是一个意思,但是用法不一样;
2.append函数的定义: func append(s []T, vs...T) []T
3.append函数的实际用法: s = append(s,1) //其中s是slice;可同时追加多个值
4.当追加的元素个数超过了slice当前可以包含的最大个数cap(slice)时,Go会给slice重新分配一个array,并且将返回的slice指向这个新的underlying array(这个应该就是原先那块地不够存了,然后根据返回的slice的长度重新找一块地存储它,并且将slice指向新的array,也就是说如果返回后的slice的长度如果超过了当前的undelying array的长度后,slice append之前的&slice[0]和slice append之后的&slice[0]已经不是一个值了):
func main() {
age := [3]int{1,2,3}
fmt.Println(&age[0]) //原始数组第一个元素的地址
sli := age[0:1] //原始sli,里面只有一个元素len(sli)=1,cap(sli)=3
fmt.Println(&age[0], &sli[0]) //因为sli的len,还未超过基础数组的大小,所以输出值同上
sli = append(sli,4) //向sli追加1个元素,此时len(sli)=2,cap(sli)=3
fmt.Println(&age[0], &sli[0])//因为sli的len,还未超过基础数组的大小,所以输出值同上
sli = append(sli,4) //向sli追加1个元素,此时len(sli)=3,cap(sli)=3
fmt.Println(&age[0], &sli[0])//因为sli的len,还未超过基础数组的大小,所以输出值同上
sli = append(sli,4) //向sli追加1个元素,此时len(sli)=4,cap(sli)这个大小目前还不知道,因为不清楚,重新分配后的基础数组在原数组的大小上做加法运算的规则
fmt.Println(&age[0], &sli[0]) //此时因为len(sli)已超过了3,所以重新分配了基础数组的位置,而sli又是对基础数组地址的引用,因此此处的第0个元素和上面不同了
fmt.Println(&age[0], age)
fmt.Println(&sli[0], sli)
}
5.看到这记录一个对比, 使用slice[index]方式赋值的话, index的大小不能超过length; 而使用append(slice,elements)追加值的话, 参照的就不再是length了, 而是capacity, 当追加后的长度未超过该slice的capacity,则slice引用的是原始array在内存中被分配的位置; 一旦超过了capacity,则Go就会重新找一块地放array(新开辟的内存是以sli进行填充还是以sli所基于的老arr进行填充?感觉应该是前者,因为用原始的arr进行填充没啥意义,反而当sli如果不是从索引0开始的话, 还会浪费空间.),然后这个slice的underlying array就不再是原始的那个而是新分配的这个(直观体现就是首元素在内存中的地址不同了). 具体一点来说, 当sli中的元素个数未超过3时,都是在给age分配的内存里玩, 当超出age的空间了, 就会开辟一片新内存, 然后将age当前数据copy过去,然后将新元素追加到新空间中, 之后再追加就没age啥事了,也就是从append函数导致"out of range"后, age就停止不前了,数据也不会再变了, 一些后续操作都是在新开辟的这块空间上进行的,这也就导致了最终输出age的结果是[1,4,4],而sli的结果是[1,4,4,4].
Range
1.for循环中的range形式用于遍历一个array(index,value), slice(index,value), map(key,value)或是一个channel(value)
2.当使用range遍历一个range或slice的时候,每次遍历会返回两个值,index和与之对应的value(文中特别强调那个值是slice相应位置的一个copy,说白了就是告诉我们,对slice的遍历并不会影响原slice的内容,即并不是说将0位置的索引值返回后,这个值就从原slice删除了.这一点可以和python中使用readline()或readlines()函数从文件对象中读行形成鲜明对比,如readline()函数是调一次文件对象中就少一个元素,而readlines()调一次文件对象直接就空了); 而range对map的遍历同样是返回两个值,只不过一个是key,一个是key对应的value.
3.for循环中对slice使用range的形式: for i, v := range slice { statements} //需要注意的是在for和range联用时,不能将i,v:=range slice看成是上面讲for循环三个部分的第一部分,而要认为for语法有两种形式,第一种形式就是常规的由两个分号隔开的三段式; 第二种形式就是由range主导的形式.如果你强行加上i,v:range slice;condition;x++|x--是会报错的,具体后面还要看一下for和range联用的语法中有没有写其它的.
*.for循环中对map的遍历形式同对slice或array的遍历,唯一不同的是slice或array返回的两个值是index和对应的value; 而map返回的是key和对应的value.
Range continued
1.刚才说过for循环中的range slice形式,会返回两个值,一个是index,还有一个是和index相对应的值;如果我们只想要index,则可以将value的位置替换为_,反之,如果只想要value,则可以使用_代替index:
for i, _ := range slice
for _, value := range slice
2.如果只想要index值,还可以直接将",value"直接省略,即:
for i := range slice
Exercise: Slices
1.做了, 但由于不懂图像相关的知识, 最后也是不明所以.做的时候参考了链接: https://pkg.go.dev/golang.org/x/tour/pic#example-Show
Maps
1.map是由多个键值对组成,这个和其它语言中的字典,哈希啥的没区别
2.map使用的时候有以下几种情况:
a.声明但未初始化的map,其值为nil,最关键的是未初始化的map不能被赋值:
var m map[string]int //之前这句写的是 m := map[string]int 仔细一看海象符出现,则必须要赋值,所以海象符的这个写法是不合法的
m["age"] = 18 //上面这两句是不正确的写法,因为上句m声明时未被赋值,m的值为nil,此时不能直接给m赋值
当m值为nil的时候,通常的用法和slice是一样的,即放在if语句中判断其值为nil:
if m == nil { fmt.Println("nil!")}
b.声明但未初始化的map,如果也想被赋值,其中一种方法是使用make方法返回一个同样类型的map对象:
var m map[string]int
m = make(map[string]int) //注意,此时,中间的是等号,而非海象符
m["age"] = 16 //经过上句make函数这么一折腾,此时的写法就是正确的
c.可以直接使用make函数返回一个可被赋值的map对象
m := make(map[string]int)
m["age"] = 16 //此时的写法是正确的,因为make函数的参数为map类型时,返回的就是一个可直接操作的map.
d.声明的时候直接将map进行初始化则后面可以直接给这个map赋值:
*.声明的时候直接初始化,应该说的就是上节要讲到的的map literal, 它是得到可操作map对象的一种手段.
m := map[string]int{"zhangsan":19}
m["liSi"] = 20 //这种写法是正确的,因为上句m在声明时赋了值,m的值不再是nil,故后面可以直接向m赋值
e.还需要注意的就是当map的数据类型是稍复杂一点,如结构体的时候,初始化和后面赋值时的写法:
type Vertex struct { Lat, Long float64 }
声明变量m的同时赋初值:
var m = map[string]Vertex{
"zhangsan":Vertex{80.6,-90.9},
"lisi":Vertex{90.6,-80.9},
}
*.此时初始值中类型名Vertex可以去掉,海象符声明时就赋值也可以省,这种方法就是前面提到过的因式分解法,factored;还可以换种想法, 编译器会自动根据T进行补全. 但使用var和make组合得到的变量,在使用m[key]=value的形式进行赋值时,value实例中,T不能省,因为此时你是单独赋值,编译器没有参考的东西.
未初始化但使用make函数得到了可操作的map对象:
var m map[string]Vertex
m = make(map[string]Vertex)
m["Bell Labs"] = Vertex{ 40.68433, -74.39967, } //重要关注的是这句,即Vertex作为值时的写法
同样,复杂的结构也可作为map中的键:
type Vertex struct { X,Y int }
var m map[Vertex]int
m = make(map[Vertex]int) //这句和头一句也可写为一句 m := make(map[Vertex]int) 或声明时就进行初始化,写为 m:= map[Vertx]int{Vertex{2,3}:4}
m[Vertex{1,2}] = 3
fmt.Println(m[Vertex{1,2}])
*.无论类型多复杂,只要记住,实例化的语法是"类型{值...}"的形式就可以了.特殊一点的就是声明类型的时候如果一并赋初值了,则此时类型实例化的时候,可以省略类型名,T,只写一对花括号,{}.
*.再强调一点, 对于结构体的声明: 如果左右花括号和字段都在一行,则字段间要用分号隔开; 如果左右花括号单独位于一行,每个字段也单独位于一行,则字段间的分隔符为换行(也就是啥都不用加,因为单独位于一行就隐含着换行符的存在)
对于结构体的实例化:字段间要使用逗号隔开,最重要的是如果最后一个字段和右花括号在一行,则最后一个字段后可以不加逗号;如果最后一个字段和右花括号不在一行,则最后一个字段后要追加一个逗号,否则会报错.
Map literals
1.map literals指的同上边一样,即在声明时赋值从而得到相应的map对象.之前说的是上面讲的struct literals不同于其它的literals,当时的想法是看代码中先有type Vertex struct {X,Y int},然后下面才是var x := Vertex{1,2}就觉得struct literals和其它类型的literals不同了, 但其实是我想错了, 重要的一点是,所谓T literals的前提是你得先有T才行啊,你有了T下面才能谈T的literals,之前之所以理解错误,原因在于slice,array这些都是内置的类型,可以直接用,而Vertex是我们自定义的类型.
2.这一节的map literal看例子也是在声明时一并赋值,看一下例子中的部分代码:
type Vertex struct {
Lat, Long float64
}
var m = map[string]Vertex{
"Bell Labs": Vertex{
40.68433, -74.39967,
},
"Google": Vertex{
37.42202, -122.08408,
},
} //该例子在对Vertex实例化的时候,语法为Vertex{X值,Y值},对比于匿名结构体的初始化, 要使用struct{变量名 类型}{变量值}的形式(当然对于海象符声明变量以及var声明时赋值时,元素实例中的匿名类型可以因为因式分解规则的存在,而不用去写),整体去想就好理解了,对自定义的类型进行实例化的语法就是 类型名{具体值}, Vertex就已经是类型名了, 所以其实例对象可以直接为Vertex{值}; 而匿名结构体,因为没有别名用来表示其类型,所以在实例化的时候就要把struct{变量名 类型}写上,然后后面跟上{值},即 struct{变量名 类型}{值}
Map literals continued
1.下面的例子是官文给的,map实例化的时候top-level type可省时的代码:
type Vertex struct {
Lat, Long float64
}
var m = map[string]Vertex{
"Bell Labs": Vertex可写可不写{40.68433, -744.39967}, // "Bell Labs":[Vertex]{40.68433,-744.39967}本来想写这种形式,用[]表示是可选的,但[]在编程语言里又比较敏感怕引起混淆,所以干脆就用大白话表示了
"Google": Vertex可写可不写{37.42202, -122.08408},
}
*.这里其实主要就是搞不清楚top-level type是个啥玩意儿,一开始认为是在函数外声明的类型就是top-level type,因为go中没有类的概念,所以也谈不到在某个类内部声明.但是通过测试无论是在main函数里面还是外面声明Vertex,m在实例化的时候,相应键值中Vertex关键字都能省.所以之前想的肯定不对,结果上网上搜了搜,有个人还专门为弄不懂这个东西去开了个issue,后面他也是在stackoverflow上提问得到了相对满意的答案(另外该链接还给出了另一个字典值可省略类型名的情况,即在声明字典类型时,map[int]struct{x,y string},这样直接将类型声明嵌到另一个类型声明中,因为该struct结构体没名,故实例化的时候键值处直接{}即可),这个答案链接为: https://stackoverflow.com/questions/47579004/what-can-be-used-in-a-map-literal-instead-of-a-type-name-in-go 大概意思是:
基本上讲的是当定义了一个接口I时,如果后面类型A实现了I(中的方法),类型B也实现了I(中的方法),则当给一个为[int]I的字典实例化的时候,就要明确的在键值处写为A{},还是B{},因为如果就一个A类型实现了接口I(中的方法),则你在实例化的时候写作3:{},则go编译器会根据你声明的定型直接将{}处理为A{},但现在有两个类都实现了接口I,此时就得你显式的给出来这个类型了:
type I interface {ok() string}
type A struct{}
type B struct{}
func (a A) ok() string {
return "a"
}
func (b B) ok() string {
return "b"
}
func main() {
m := map[int]I{
3: A{},
4: B{}, //注意这样写的话末尾的逗号要有,否则会报错,上面也有提到,如果结尾花括号不单独另起一行,则最后一个元素不用尾缀一个逗号
}
fmt.Println(m[3].ok())
fmt.Println(m[4].ok())
}
但是按照stackoverflow上的逻辑来看, 如果我只定义了一个实现了I的类型,则m中实例化的时候,键值部分就能写{}而不用前缀个A或B了,但实际情况不是这样的:
type I interface {ok() string}
type A struct{}
//type B struct{}
func (a A) ok() string {
return "a"
}
//func (b B) ok() string {
// return "b"
//}
func main() {
m := map[int]I{
3: {},
// 4: B{}, //注意这样写的话末尾的逗号要有,否则会报错
}
fmt.Println(m[3].ok())
}
上面的代码是报错的,说"missing type in composite literal",而将 3:{} 改为3:A{}后,执行就正常了.
通过上面这两段代码我觉得stackoverflow中说的和主题不符.我现在又有了新的理解,也是通过stackoverflow上的这个例子和官文中定义的Vertex结构体类型共同找到的灵感,即因为I是一个接口,可以被很多类型实现,所以在给它传值时,就一定要明确指定哪个实现了I的类,即使当前只有一个类实现了I中的方法.但像之前定义的Vertex类,因为go中没有继承的概念,所以一说到Vertex就只有一个,唯一的一个,它没有子类啥的,因此即使没有明确在{}前给出类型名,golang编译器也能推断出来.这可能就是所谓的top-level type吧,当然这是猜的.而在给字典赋值时又出现了问题:
func main() {
type Person struct {
Age int
}
var d map[string]Person = map[string]Person{
"zhangsan":{18},
"lisi":{19},
}
delete(d,"zhangsan")
fmt.Println(d)
d["zhangsan"] = Person{20} //这句
fmt.Println(d)
}
上面代码备注那句在给"zhangsan"赋值时,后面一定要写成Person{20},而不能写成{20}, 对于这个例子我的理解是,在给字典d初始化赋值时,会考虑到字典的键或值的实际类型,然后看这些类型是否为top-level type(按照上面最后一句我自己的理解走),去判断赋值时这个类型关键字是否可省. 而一旦初始化完成了,后面使用得到的对象进行赋值时,就不会考虑那么多了,就直接是拿你写的类型值去和d定义中的比较,正确则执行,不正确则报错,即使用初始化后的对象对字典进行更改时,就没有了推断的过程,只有比较的过程,因此此时的Person必须要写.
*.对于top-level type的理解见: https://stackoverflow.com/questions/47579004/what-can-be-used-in-a-map-literal-instead-of-a-type-name-in-go,简单来说就是当类型为接口时,实例化时要明确指定类型,因为多个类都可实现同一个接口,所以当你声明一个变量类型为接口时,实例化的时候就一定要指定到底用的是哪个实现该接口的类. //2023-12-12理解的top-level type: 不能有先类的类型, 如自定义的结构体; 像接口类型就就可以有先类, 即接口B可以有接口A作为先类. 578行chatGPT给出的解释中就把接口类型作为了top-level type, 所以现在还是不太理解top-level type的定义到底是啥,不过不影响当前的学习, 因为这块无非就是实例化的时候类型名能省不能省.
**.这里对本小节来个总结吧
1.先说接口,实例化的时候,类型T一定要存在;
2.再说top-level type,这个概念在这小节里最让人摸不着头脑, 但从例子来看,可以忽略到这个概念的存在, 讲的就是上面反复讲到的,声明变量时赋值,则因为有因式分解规则的存在,实例中的类型T可省; 而单独给字典中的某个键赋值时,类型T必须存在.
**.chatGPT给出的顶级类型描述(没有详细的去验证是否正确):
在 Go 语言中,顶级类型(Top-level type)指的是不嵌套在其他类型中的类型,是最顶层的数据类型。Go 语言中的顶级类型包括:
基本类型:bool、int、float、complex、string 等。
结构体类型:结构体是 Go 语言中的一种复合数据类型,它由一组命名的数据字段组成。
数组类型:数组是一种固定长度的数据结构,存储多个同类型的数据。
切片类型:切片是一种变长的数据结构,可以动态地存储多个同类型的数据。
接口类型:接口是一种特殊的类型,它可以定义一组方法的签名,但不实现这些方法。
函数类型:函数是一种可以被调用的代码块,它接受参数并返回一个或多个值。
顶级类型是 Go 语言中的基本数据类型,它们用于表示程序中的数据和状态,是编写程序的基础。任何复杂的数据类型都是由这些基础类型组合而成的。
Mutating Maps
1.强调一点,变量要先声明后使用,或者在使用的时候直接声明,就第二点来说,比如elem和ok之前都没有声明,则可以在使用的时候使用短声明的方式直接声明: elem, ok := m[key]
2.这节主要讲的是map是可变的嘛,所以下面几点主要围绕着可变来说:
a.改变某个键的值 m[key] = new value
b.检索某个键的值 elem = m[key] //前提是elem在前面已经声明,如果前面没有声明elem,则可以使用短声明的格式在赋值时声明: elem := m[key]
c.删除键值对,其实只要删除key就相当于将整个键值对都删了: delete(m, key)
d.测试某key是否在map m中: elem, ok = m[key] //用这种形式的前提是elem和ok已经在前面声明了(如果前面没声明,要写成elem,ok := m[key]);就该例来说,如果key在m中则ok的值为true,elem为相应的值,如果key不在m中,则ok为false, elem为m中元素类型的"零值"
*.有一点特别需要注意: 声明结构体时,如果多个字段的声明在同一行,则字段间的分隔符为"分号"而非"逗号",而在结构体初始化时,实际的字段值间的分隔符为"逗号",而非声明中的"分号".
Exercise: Maps
实现代码:
*****
package main
import (
"strings"
"golang.org/x/tour/wc"
)
func WordCount(s string) map[string]int {
fields := strings.Fields(s)
wordCount := make(map[string]int)
for _, word := range fields {
wordCount[word]++
}
//和原文给出的待补全代码相比,这句给改了,因为wc.Test给的是随机字符串,而原文给的是具体的{"x":1}肯定不会过的.
return wordCount
}
func main() {
wc.Test(WordCount)
}
*****
Function values
0.functions are values too. //对于理解非常重要的一句话, 函数值是"函数作为值",即有参数类型是函数类型和或返回值是函数类型(换句话说函数也是一种变量类型), 而不是"函数的返回值"
0.从这节的解释说明来看,function value指的是两个函数: 1是作为参数传给高阶函数(higher-order function)的那个函数; 2是作为高阶函数return返回值的那个函数 //还是上一句概括的更全一些,即function value想表达的就是同整型,字符串类型,结构体类型等一样, 函数其实也是一种类型, 可以出现在变量定义中, 可以作为函数形参类型, 可以作为函数返回值类型等.
1.初看,函数既可以作为函数参数,也可以作为函数的返回值, 应该就是其它语言中所谓的高阶函数吧?! //之前说类似于python中的decorator,其实也不一样, python中对于decorator的解释是 参数和返回值都是函数,是和的关系(可以看cnblogs上关于python中decorator的笔记), 而这里的函数值说的是函数也可以作为值进行流转, 可以是和的关系也可以是或的关系, 即函数值出现在哪个环节都行,也可以同时出现.
2.再看,有一点需要注意的,就是函数作为参数时的写法,这块乍一看不理解,但是拆开来看就好理解了: func compute(fn func(float64, float64) float64) float64{},分解开看一下:
func是定义函数的关键字
compute是函数名
fn是compute中的参数
func(float64, float64) float64整体是fn的类型
*.这点就是我要说的,正常情况下一个函数是由函数名,参数和返回值类型组成的,上面这条完整的表现了一个函数的这几点要素。而fn的类型是函数,所以这样子去理解"fn func(float64, float64) float64"就不难理解了
*.再看例子,觉得上面这个*说的就有点不对了,"fn func(float64, float64) float64"当函数作为参数时,fn是参数名没错,"func(float64, float64) float64"作为fn的类型也没错,但是注意到一个细节,当函数作为参数类型时,相应的结构中没有函数名,这个是可以理解的,而且形参也是只有类型没有名,这个应该也理解,没了,都可以理解了,最后的那个float64是func(float64, float64)的返回类型
3.还是将tour上的那个例子写在这吧,看着例子好理解一些:
import (
"fmt"
"math"
)
func compute(fn func(float64, float64) float64) float64 {
return fn(3, 4)
}
func main() {
hypot := func(x, y float64) float64 {
return math.Sqrt(x*x + y*y)
}
fmt.Println(hypot(5, 12))
fmt.Println(compute(hypot)) //这块函数引用只用了函数名,需要注意一下;另外,compute中, return返回的是fn(3,4)而不是fn,也就是说return返回的是fn(3,4)的返回值,因此compute(hypot)返回的其实是hypot(3,4)的返回值,float64类型的math.Sqrt(3*3+4*4),即最后返回的其实不是一个函数, 而是一个值.
fmt.Println(compute(math.Pow))
}
*.关于上面这个高阶函数,还要讲一下当函数A作为另一个函数B的参数时,在调用时的注意事项,见下面代码main函数中的注释部分:
package main
import "strconv"
import "fmt"
func A() int {
return 86
}
func B(fn func() int) string {
return strconv.Itoa(fn())
}
func main() {
fmt.Println(B(A)) //方法一,直接传入函数名,切记函数名不要加(),否则被传入的类型就不是函数A了,而是函数A的返回值(int类型)
x := A
fmt.Println(B(x)) //这个方法基本同上,就是给函数A取了个别名
y := func() int {return 86}
fmt.Println(B(y)) //这是官文推荐的方法,即不用func声明函数,而是直接将函数作为一个表达式,最终得到一个函数类型的变量y
}
4.假设A是一个高阶函数,其参数和或返回值为函数B,当时想即然函数B无论如何也是要在函数A中执行的, 那为什么不直接在函数A中直接调用函数B呢,反而多此一举,先将函数B作为函数A的参数,然后再在函数A中对B进行调用?!, 现在我想明白了, 关键点在于"动态"两个字,假设我们明确知道要在函数A中调用的函数B是谁,那么无论是使用高阶函数还是直接在函数A直接调用函数B都是没有问题的. 但问题是, 我们将函数A给抽象成可以在函数体中执行任何函数B的函数,此时就必须要用高阶函数了,因为这个函数B不是特指某个函数.知道了这点,再一想一下常用的map函数,它接受一个数据集,和一个处理数据集的函数, 此时你再想想, 100个人可能有100种处理数据集的想法, 你怎么能做到直接在map函数中调用一个函数逻辑去满足这100个人的处理逻辑呢?此时只能使用高阶函数,将map函数的参数类型设置成函数类型, 这样一来你可以传入任何满足声明条件的函数, 从而可以用不同的逻辑处理数据集. 如果golang本小节是要讲高阶函数,其实它举的这个例子对于理解什么时候使用高阶函数是没有帮助的, 这只谈到了用法, 但这个例子执行完过后,脑子里并不知道什么时候会用到这种结构.
*.本小节最后想再强调一下声明函数类型以及给函数类型传值的常见用法: 在声明函数类型时,通常形参和返回值是没有名称的,只有相应的类型,因为目的已经达到了, 我函数里想要几个参数,参数类型是啥,返回值类型是啥,意思已经表达清楚了. 而给这个函数类型传值时,一般就要给形参命名了, 因为通常情况下我们要在函数体中对形参进行引用.其实可以通过上面讲过的所有类型的声明和实例化方式去理解函数类型的声明与赋值,函数的声明就如上面说的,用最简单的形式表达清楚要素; 而函数的赋值说白了就是把函数类型给写完整了, 即参数类型该补的补上, 函数体中的语句该补的补上.见例子:
func FunctionAsArg(f func(int) string) { //函数类型形参及返回值部分仅给出要满足的类型
}
var fn = func(x int) string { strconv.Itoa(x) } //传值的函数将形参名以及函数体都补全了
FunctionAsArg(fn(888)) //实际调用
Function closures
1.什么是closure(可以参考javascript中的闭包, 搜mdn closure)?
::closure是一个函数值,我感觉再精确一点,closure就是一个作为(返回)值的函数;但是还有一个约束,就是这个函数返回值的body部分引用了body外的变量,body外应该指的就是和return同级的一些变量;这句话还可以和上一句一样说的更精确一些,closure其实就是一个无名函数,它作为函数的返回值,return的参数。即函数的返回值也是一个函数,但是还有一点需要说明的是不是随便一个被返回的无名函数都叫closure,这个无名函数体内还要引用和return同级的变量才可以;总结一下closure的特点:
a.closure函数是一个无名函数;
b.closure函数是某个函数的返回值;
c.这个内部的无名函数体中要引用父函数中的变量
d.closure一般作为return的参数
*.再补充一点,拿js中的closure来对说, 闭包和垃圾回收相关联, 简单来说由于返回的函数引用了其所在函数中的变量, 导致这个外部函数的内存没有被回收,从而导致多次调用返回的这个内部函数可能会产生不同的结果. 举例来说, 外部函数定义了一个变量a:=0,而内部函数只有一条语句为a+=1,假设被返回的函数类型值为 return f(),则你会发现,同一个函数,第一次调用f()得到的值是1,第二次得到的值是2.
=====
package main
import (
"fmt"
)
func TestClosure() func(){
a := 0
return func(){
a += 1
fmt.Println(a)
}
}
func main(){
f:=TestClosure()
f() //output 1
f() //output 2
f() //output 3
}
=====
2.这节通过例子还是有两点需要注意的:
a.同时给多个变量赋值,这个其实也不算新东西:
pos, neg := adder(), adder() //准确来说应该叫同时声明多个变量并赋值
b.下面这个例子讲到了精髓,在下面的例子中,所谓的function closure并不是adder()这个函数, 而是它的返回值"func(x int) int",而重点在于返回值中的函数引用了outer的变量sum,而closure function的作用可以理解为: 将外部函数adder理解为一个类,将每次adder()理解为得到一个实例, 类的实例说白了就是在将这个类复制到另外一块内存上嘛.使用adder()多次赋值就会得到多个实例,这些实例之间没有交集,是在不同的内存区域上, 但对相同实例的多次调用时,上一次的调用可能会影响下一次的调用,因为两次调的是同一个实例嘛,所以下一次调用时会引用上一次得到的sum;(这么说可能不是真正的设计逻辑,但是对于前期理解还是有一点帮助的)//这段话要是不明白根据官网例子的结果多推敲一下就明白了
func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}
func main() {
pos, neg := adder(), adder()
for i := 0; i < 10; i++ {
fmt.Println(
pos(i), //0,0 | 1,-2 | 3,-6 | 6,-12 | 10,-20 | 15,-30 | 21,-42 | 28,-56 | 36,-72 | 45,-90
neg(-2*i),
)
}
}
*.你可以将adder()理解为类, pos,neg := adder(), adder()相当于类adder的实例化,得到两个实例pos和neg, 此时因为是两个实例,所以操作pos时对neg是没有影响的, 反之亦然, 但正如上面例子所说, 多次操作同一个实例时,如果多次调用都会用到同一个变量(引用closing function中的),则前面调用时产生的结果会影响后面调用时该变量的初始值,原因很明显, 同一个实例中,同一个变量在内存中的地址是固定的.//这段话也可以理解为function closure(函数闭包)的使用场景
*.上面关于function closure的说法应该不完全正确, 感觉只是其中的一种应用形式.//但function values一节说function values(function value从形式上看其实就是一个完整的函数)可以用于函数参数或函数返回值, 而这一节又说在function closure中引用closing function中的变量,所以只有匿名函数作为返回值才满足可以在函数体中调用closing function中变量的情况(这句话说的不完全对, 在函数内声明一个函数类型的变量也然后将该函数类型的变量作为返回值也满足闭包函数的要求,只不过内嵌函数不能用func的形式声明,只能用海象符的形式,如inner:=func(){}).
*.也就是说closure function指的是:"a.内嵌于函数A中","b.作为return参数"并且"c.引用A中变量"的" //返回值的形式可以是reutrn func(){} 也可以是先用海象符声明inner:=func(){}, 然后再return inner.即,可返回有名函数也可返回匿名函数, 而可以看到, 这里的有名函数其实也是由匿名函数得来的, 所以本质上还是匿名函数.
**.在看net/http/server.go的源码时发现了函数的另一种用法:
import "fmt"
func main() {
type X func(int,string)
Q := X(func(y int, z string){fmt.Println(y,z)}) //还可写为var Q X = func(y int, z string){fmt.Println(y,z)}
Q(3,"6")
}
上面看到的写法是直接将匿名函数作为变量值,即 a := func(){...}, 之后便可以使用a()来进行函数调用; 而上面这个例子中是先声明一个函数类型的变量X, 用的时候有点不一样,将匿名函数作为X的参数类型,而得到的函数类型Q,可以像Q(y,z)这样使用. 这样的好处是,声明了一个函数类型,我们可以用这个类型去扩展一些方法.而像刚刚说的直接将函数作为表达式的情况就不能用得到的a去扩展方法.
*.对照上面的这个自定义函数类型的写法,我又找了个例子,引发了我的思考,对比上下这两个例子,对于由type自定义的类型到底应该如何赋值?:
//type app []int
func main() {
//a := app{1,2,3}
//var a app = []int{1,2,3}
//var a app = app{1,2,3}
//var a app = app([]int{1,2,3})
//fmt.Println(a)
}
*.通过上面这两个例子可以看出,当我们自定义一个类型的别名时, 还可以通过 别名(实际类型实例) 的形式得到一个实例. //再强调一下, 函数类型的实例,从字面看就是把函数类型补充完整(主要体现在形参有名了,也有函数体了).
*.另外当变量类型相对复杂时, 我们可以使用type关键字定义一个别名,在之后的语句中,使用这个别名来代替相应的类型:
func x(f func(int)string) {} 等价于下面两句
type F func(int)string
func x(f F){}
*.目前来看, 某类型对象, 某类型实例, 某类型值这三个概念说的是一个玩意儿, 说的就是T value, 而得到T value的手段通常是T literal.
Exercise: Fibonacci closure
1.使用closure法和array法两种方法实现,代码已放到cnblog上
Methods and interfaces
Methods
1.Go语言没有类这个概念
2.虽说Go没有类的概念,但接下来引出Go语言有方法的概念,我们知道方法这个词一般是对类中函数的称法;既然Go没有类,那么Go中的方法是怎么实现的呢?简单来说就是在原有函数定义的基础上,在func关键字与函数名之间加一个在Go中被称为receiver的参数,这么个组合在Go中就叫做(给某个类型定义了一个)方法. 再说细一点就是方法和它的接收器类型其实是捆绑在一起的, 这么说的原因是,通常, 方法中的语句都会引用类型定义中的字段.当然不引用也没关系,如方法中就是输出一个常数,这在语法上也是没问题的.
3.从实例来看,这个receiver仅仅是为函数体提供引用数据,再说白点就是receiver中定义的数据,在相对应的函数中都可以使用
4.下面这段代码叫做:Abs方法有一个类型为Vertex名称为v的接收器:
type Vertex struct {
X, Y float64
}
func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
将引用的部分也写下来吧,应该有助理解:
func main() {
v := Vertex{3, 4}
fmt.Println(v.Abs())
} //从调用形式来看,和kotlin中类的扩展意思差不多,可以看一下引用,是将Abs()作为v这个类的方法去引用的,而且Abs()函数体可以使用v类中的成员(或叫字段)
5.一个简单的例子:
package main
import "fmt"
type Person struct {
name string
age int
}
type Car interface {
drive() string
stop() int
}
func (p Person) drive() string {
return p.name
}
func (p Person) stop() int {
return p.age
}
func main() {
var YQ Person = Person{"YangQiang",30}
var baoshijie Car = YQ
fmt.Println(baoshijie.drive())
fmt.Println(baoshijie.stop())
}
上面这个例子将接口,interface,也给带出来了, 如果要说interface的常用方法,上面这个例子还不够,如果再定义一个type Alien struct{}类型,然后Alien类型再实现drive()和stop()方法,这样,"var baoshijie Car = "后面的值就即可以是YQ,也可以是Alien了,因为这两个类型都实现了Car这个接口的方法,而具体baoshijie的值是YQ还是Alien,就根据实际需求了. //抽象出来说明就是: 接口中定义的方法可以被N多的类型实现, 但不同类型对接口中定义的方法有不同的实现, 当一个变量声明为相应的interface类型时, 可以根据实际情况将相应实现其方法的类实例赋给该接口类型变量.
*.也就是说这节说的receiver, 说白了就是一个"类"!
Methods are functions
1.这一节就着重强调了: method is just a function with a receiver argument
2.拿一个例子来说明了,将method的receiver部分放到function的参数位置,则function和method实现的功能是一样的(不知道是否所有的method和function都可以这样转换,这个后期再讨论)
type Vertex struct {
X, Y float64
}
方法的形式:
func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
引用的时候:
v := Vertex{3, 4}
fmt.Println(v.Abs()) //Abs()里面没有参数,因为Abs()函数体可以直接从v中取
//这遍看突然发现,方法的调用好像不对,打印函数中参数应该是v.Abs(),现在只写了Abs() //已在Abs()前补上receiver v
函数的形式:
func Abs(v Vertex) float64 {
return math.Sqrt(v.X*v.* + v.Y*v.Y)
}
引用的时候:
v := Vertex{3, 4}
fmt.Println(Abs(v)) //Abs()里面要加参数v,否则函数体中引用的v.X和v.Y就会报错了
*.还是上面说的,就现在的理解method之于function来说只是给function加了一个环境(用环境这个词好像不太形象,理解就好。),这个环境的数据在函数体中可以引用,仅此而已(强调:是目前这样理解!)
Methods continued
1.前面讲的方法,接收器的类型是struct,其实对于非struct类型也是可以的
2.receiver的类型必须是在method所在的包内声明,在其它包中声明的类型(包括内建的包)不能作为本包method中receiver的类型 //举例来说, 像int, float64这样的内建类型,因为它们不是在你定义方法的那个包中声明的, 所以你不可以直接将它们作为接收器类型,如果确实想用其它包中或是内建的类型, 首先要保证该类型在本包内可访问(这主要是针对非内建包,因为内建的许多类型所在的包都是默认导入的),然后再使用"type 别名 非本包内声明的类型名"的方式,在本包内声明一下, 然后再使用这个 "别名" 作为某方法的接收器类型就可以了.
3.定义一个类型,使用关键字"type,后接类型别名,最后接类型" ,如:type myfloat float64
type MyFloat float64
func (f MyFloat) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}
*.通过这节官文中的例子可以知道,在实例化一个类型时, 还可以通过 类型名(实例)的形式, 拿上面这个小例子来说, 你要得到一个MyFloat类型的变量,可以写做 mf:=MyFloat(10);再比如var name struct{Name string} = struct{Name string}(struct{Name string}{"张三"})
**.在实例测试本小节的时候,发现了一个自己的误区, 之前认为"type X T"这种结构中, X仅仅是T的别名,也就是说在自己的印象中X等价于T. 但实际上并不是这样的, X是作为新类型单独存在的,是新声明的. X和T相同的地方为可以被同一个实例赋值,因为本质上都是T类型; 不同的地方在于二者作为receiver去引用方法的时候,X就是X,T就是T.因为虽说X和T的本质相同, 但要注意的是关键字type声明的其实是一个新的类型,而并不是某个类型的别名. 而你在定义方法的时候,接收器只能是一个,所以在调用方法的时候,一定要注意使用的接收器类型是X还是T,不能混用.
=====
package main
import (
pig "hello.world/one/animal/two"
"fmt"
)
type X pig.Fn
func main(){
var f pig.Fn = func(x int) pig.Person { //类型一定是pig.Fn,不能是X
s := make([]struct{Name string;Age int},1)
s[0] = struct{Name string; Age int}{Name:"张三",Age: x}
return s
}
result:= f.Output(40) //如果f类型为X,则这句会报错,因为方法Output的接收器类型是pig.Fn
fmt.Println(result[0])
}
=====
*.根据上面的例子可以知道, 虽说接收器类型的定义要和方法的定义在同一个包中, 但对方法的引用,是可以在另一个包里进行的. //注意方法名首字母和接收器类型名首字母要大写,这是Exported name一节讲的最基本的东西了
Pointer receivers
1.这一节的主题叫"指针接收器",该指针和c中的指针意思是一样的,后面应该会讲Go里没有指针之间的运算
2.其实重要的是要理解指针的概念,指针的概念弄明白了,这一节自然也就没啥了.首先说内存,可以把内存想象成一个大房子,里面有N个房间,每个房间都有自己的门牌号,1,2,3... .再来说变量,编程语言中声明一个变量,其实就是在内存中给这个变量分配一块空间,空间大小由变量的类型决定。变量声明其实最终这个变量得到的是一个内存中的地址,拿上面的房间举例来说就是,var a int,对应下来可能就是将第一间房间的房号1给变量a,而第1,2,3,4间房是分配给a的空间(具体分配几间房是由变量类型决定的),以后再给变量a赋值时其实是先定位到1这个位置,然后向1234这四个房间记数据,从a中取值也是,先定位到1这个位置,然后把数据取出来;下面该说指针了,这个指针对应的就是这个位置1,说白了指针就是内存中的地址。而指针变量就好理解了,就是这个变量中存的值是指针,再换个说法,变量中存的是内存中的地址。而指针变量前加上*,指的就是指针变量中的地址指向的值,这句说的不太明白,举例来说,变量a在内存中的地址是1,变量a的值是"abc",有一个指针变量b,b中存的是a的地址,则b的值是1,*b是"abc"(即指针变量b的值,a,所指向的内存空间的内容)
*.指针这个东西其实只要明白了内存结构,还有声明变量的实际情况就很简单,有时间要回看一下王爽的16位汇编。上面这些描述不是太准确,只能帮助理解一下
3.Go中指针最重要的一点就是,接收器如果是指针引用,则会修改指针变量值所指向的实际内容。下次再引用该指针变量值的时候,该指针变量值的指向就已经是修改后的了;如果接收器就是普通类型的变量,则方法中引用该接收器变量内容实际上是先对该内容做个备份(将变量指向的内存中的值复制一份,再重新分配一块内存存放这个备份),然后引用的是备份的内容。下次再引用该接收器的变量时还是原值,因为之前根本就没有对接收器的原始值做修改
*.这一节说的就比较不太好了,后面有机会这块用心再总结一下 //之所以感觉不好,是因为没有从内存的角度去说.从内存角度考虑的话,如果是对房间号为1的变量进行引用,则值引用是再申请个房间号2,然后把1中的内容复制到房间2里,后面的操作都是在房间2中进行的,不影响房间1中的值,后面再使用相应的变量调用方法,方法中引用变量的值还是房间1的; 而指针引用则不同, 所有操作都直接在房间1里操作,这就导致后面再使用相应变量调用方法时,可能变量中的字段值和上一次调用该方法时,方法中引用的字段值就不同了.
4.这节的例子,如果Scale接收器的类型为Vertex则最后的值是5,如果是*Vertex最后的值是50 //我的理解是,本质上还是对类的操作,我们可以将类型的声明理解为"模板",将类的实例化理解为"复制一份模板并将相应的字段初始化",指针类型的接收器特点是:"直接修改"初始化后的模板"(后面称其为实例)数据",而普通类型的特点是"将该实例再复制一份,然后在复制后的实例上做修改,即原实例的数据不受影响",此时再去看这小节的例子就可以理解了,再啰嗦一句:指针作接收器是在给实例分配的内存区域上直接做修改;普通类型作接收器是将实例再复制一份到内存的另一个区域,然后对复制后的这个实例(或叫复制后的这块内存区域)做修改,并不会影响原实例(或叫原内存区域);
5. receiver type如果是指针的话,不能是*int,*float64这些,必须是 type a float64,这里*a可以作为receiver type; 我不知道原文件咋翻译 //因为上面也提到过,一个方法的接收器类型必须是在该包声明的才行,内建的类型如果想要作为接收器类型需要再封装一层,type 别名 内置类型,此时这里的"别名"可以做接收器类型;其它包中的类型同理也要这么做. //有了上面内容的铺垫,这里的"别名"用的不恰当, type X T,这里X就是一个新的类型,只不过和T都可被同一个实例赋值,但在使用上,尤其是定义方法上, X就是X, T就是T,不能换着用.
6.func (v *Vertex) Scale(f float){}读作:定义在*Vertex类型上的方法
7.接收器的类型为指针的话,对该针指的指向进行修改,修改的是原实例的数据,即后面再引用该变量的时候,使用的值并不是一开始新建实例时的值, 而是修改过后的;但接收器的类型为普通类型的话,则修改的是这些原实例的备份,所以对实例的备份上的数据做修改肯定不会影响原实例的数据;普通函数的参数类型就属于这种(修改的是副本)
Pointers and functions
1.这节主要看的是例子,method用function实现,其实也就是将method的receiver部分给放到function参数的位置 //需要注意的是调用Scale()函数时第一个参数值为&v,因为指针变量存放的实际上是地址,所以这里用到了&符号
//记得下面会讲到,当某个类型作为接收器存在时,该接收器类型的实例引用方法时,既可使用值引用也可使用指针引用,不用考虑实际方法定义中接收器类型写的是值还是指针. 也就是说编译器在实际编译时,并不直接看你的引用形式, 而是先去看看你调的方法声明中接收器是什么类型. 如果你引用中的接收器类型变量写反了(应该是值引用你写的指针引用,反之),则编译器会自动帮你改过来; 但作为函数形参时,形参类型是值引用, 则调用时就必须用值引用;是指针引用,调用时就必须是指针引用.
*.试着从编译器的角度去理解变量,因为编译后的代码中是不存在变量名这个东西的, 也就是在编译的时候代码中的变量名实际上都被替换成了内存地址,此时fmt.Println(a),可以写成fmt.Println(内存中地址),这就好理解了,打印的是"内存中地址"处存的内容; 再来说指针, 假设有:var p *int; var int a=1; p=&a,则:
fmt.Println(a) -> fmt.Println(给a分配的内存起始地址)
fmt.Println(p) -> fmt.Println(给p分配的内存起始地址)
fmt.Println(*p) -> fmt.Println(给p分配的内存地址处存放的内存地址(即指针变量p的值))
*.另外需要注意的一点是在给函数中指针类型的形参变量传值时,这个形参变量实际上也是新建的,作用域为函数内部的一个变量.我想表达的意思是:
func TestMethod(p *Person){}
var p1 = Person{}
TestMethod(&p1) //TestMethod函数中形参p的地址不是&p1, &p1这个地址只是给作用域为TestMethod这个函数内部声明的变量p赋的值.写这个例子的原因是测试的时候认为p的地址就是&p1,结果函数体中输出的变量值和自己想象的不一样. 后面再看这段的时候可能就不明白我当时的场景到底是怎样的了, 这里也不写了, 但只要记住这个道理就行了.
Methods and pointer indirection
0.这节讲的是使用"函数+函数参数"组合,调用函数时,参数定义是pointer型就必须传pointer类型的值,不能传value类型的值; 而"接收器+方法"组合, 当接收器类型为pointer类型时,既可以给接收器传pointer类型值,也可传value类型值;反之当接收器类型为value类型时, 同样是传pointer类型值也行,传value类型值也行.
1.function中的参数是啥类型,调用该function时参数类型绝不能写错(这个不像receiver中的类型,明明类型是指针或是普通类型,但调用的时候既可以使用普通类型也可以指针进行调用,Go优化了这部分逻辑) //我写这句话其实是为了强调一开始讲指针时的一个调用例子,即本应该是(*p).X,但在Go中直接写p.X就好!但是如果指针作为函数参数而非接收器时,在调用函数时,该参数一定要用指针调用的形式而不能用普通变量调用形式(其实关键点就是那个&符号,指针参数有,而普通类型的参数没有)
var v Vertex
ScaleFunc(v, 5) // Compile error!
ScaleFunc(&v, 5) // OK
2.上面1说了,当方法的接收器类型为指针时,引用方法的时候,接收器可以使用value(普通类型)类型值,也可以使用pointer(地址)类型值 //猜测Go在处理指针类型的接收器调用函数时,如果你没在指针变量前加&的话,Go会隐式的帮你加上,这个行为猜测是编译阶段编译器处理代码时给自动加上的;但如果指针作为函数参数时,猜测由于Go语言没有在编译阶段加上这个逻辑,所以在函数调用时,如果函数参数为指针类型则一定要使用&符号,其实感觉函数参数如果是指针类型也可以处理一下,无非是处理之前先检测一下类型呗,当然啊这句注释完全是为了好理解的一个猜测.
var v Vertex
v.Scale(5) // OK
p := &v
p.Scale(10) // OK
*.作为约定,Go在解释v.Scale(5)的时候会自动解释为(&v).Scale(5)
3.还要注意指针中&号与*号的简单作用,给个简单例子看一下吧:
package main
import (
"fmt"
)
var x = 5
func main() {
var p = &x
fmt.Println(x) //5 x地址处的内容
fmt.Println(p) //x的地址 p地址处的内容
fmt.Println(&x) //x的地址 x地址
fmt.Println(&p) //p的地址 p地址
fmt.Println(*p) //5 p地址处的内容指向(*)的内容
fmt.Println(*(&x)) //5 x地址(&x)指向(*)的内容
fmt.Println(**(&p)) //5 p地址(&p)指向(*)的内容(x地址)指向(*)的内容
}
输出为:
5
0x190004 //这几个地址每次输出的值可能会不一样
0x190004
0x40e128
5
5
5
*.静下心来把上面这个例子的输出从上到下一个个的想一下, 为啥输出内容会是这个, 看看有没有什么想法或者说从这几种写法中能总结出什么规律. //光变量带指向; 地址指自身;星号表指向;
Methods and pointer indirection(2)
0.这节讲的是使用"函数+函数参数"组合,调用函数时,参数定义是value型就必须传value类型的值,不能传pointer类型的值; 而"接收器+方法"组合, 当接收器类型为value类型时,既可以给接收器传value类型值,也可传pointer类型值.
0.总结一下该小节和上一小节就是定义在函数参数中的类型,必须精确传相应类型值; 而定义的接收器类型,在给其传值时,可以随便传value型或pointer型,也就是说Golang在检测到你调的是方法时,会先去看一下你定义中接收器类型是value还是pointer, 然后再去处理你实际传给接收器变量的类型值,如果你传的和定义的类型相同,则就这样了,如果不同(即定义的是value型而给其传的是pointer型, 反之),则自动帮你转换一下.
1.上面也提过,function参数是啥类型调用时就得使用啥类型
var v Vertex
fmt.Println(AbsFunc(v)) // OK
fmt.Println(AbsFunc(&v)) // Compile error!
2.但是method不一样,method中接收器的类型为value时,调用该method时也是即可以使用value也可以使用pointer类型
var v = Vertex{3,4}
var p = &v
fmt.Println(v.Abs()) // OK
fmt.Println(p.Abs()) // OK
*.p.Abs()会被解释为(*p).Abs()
**.对上两小节的主要内容总结一下吧:
1.无论方法的接收器是value类型还是pointer类型,在进行调用时都可以使用value类型或是pointer类型进行调用, 编译器会进行相应的转换
a.接收器为value类型,则如果使用&v进行调用,则编译器会将&v转化为*(&v)
b.接收器为pointer类型,则如果使用v进行调用,则编译器会将v转化为&v
2.接1说,但是函数定义中的参数如果是value类型则传参时必须是value类型,如果是pointer类型则必须传pointer类型 //这个就不说了上面都有例子
/*当接收器类型为指针时,在调用相应的方法时,无论你是使用指针类型进行调用还是使用值调用,调用结果是一样的,都遵循"方法定义时所使用的接收器类型". 换句话说,当接收器为指针类型时,如果你使用指针类型调用,正好,这是常规逻辑; 如果你使用的是值调用,则在真正执行的时候,编译器也会根据相应的方法在定义时前面的接收器类型,给你的值调用前加上个"&"(既然说到编译器,则这么说就不恰当了,更恰当一点的说法是编译器会自动帮你校正接收器对象的地址),然后再进行执行. 也就是说,当接收器为指针类型的变量时, 你在调用相应的方法时,无论是使用的值调用还是指针调用,最后执行的语句都是一样的,即都是指针调用,因此得到结果也是一样的.就下面的例子而言,ZS声明时的类型为普通Person类型,当你引用的时候写的是:
ZS.multiply() 时, 在正式编译前会检测multiply()方法定义时的接收器类型,将该句预处理为:(&ZS).multiply(),然后再进行编译执行.
而当你写的是: (&ZS).multiply()时,同样在编译前还是会检测multiply()定义时接收器的类型,发现没问题,则预处理对它啥也不做,原样编译.
也就是说: 关键点在于golang在调用方法的时候,会根据方法在声明时接收器的类型去调整你实际调用时使用的变量类型.
func (p *Person) multiply() {
p.name = p.name + "-"
p.age = p.age + 3
}
func main() {
ZS := Person{"zhangsan",18}
fmt.Println(ZS.name, ZS.age)
ZS.multiply() //ZS-> (&ZS)
fmt.Println(ZS.name, ZS.age)
ZS.multiply() //ZS-> (&ZS)
fmt.Println(ZS.name, ZS.age)
fmt.Println(ZS)
}*/
/*下面的multiply()方法的接收器是普通类型,而下面的main()函数中,ZS变量是指针型变量,此时当我们在调用multiply()方法时写的是:
ZS.multiply(),则编译器会根据multiply()方法在定义时前面接收器的类型,自动将指针类型引用转为值引用,即改为: (*ZS).multiply().
如果你在调用multiply()时,直接写的就是(*ZS).multiply(),则编译器同样还是在执行该句前检测multiply()方法定义时的接收器类型,发现调用的类型和定义时的一样,则不会更改什么,原样执行
func (p Person) multiply() {
p.name = p.name + "-"
p.age = p.age + 3
}
func main() {
ZS := &Person{"zhangsan",18}
fmt.Println(ZS.name, ZS.age)
ZS.multiply() //ZS-> (*ZS)
fmt.Println(ZS.name, ZS.age)
ZS.multiply() //ZS-> (*ZS)
fmt.Println(ZS.name, ZS.age)
fmt.Println(ZS)
}*/
Choosing a value or pointer receiver
1.这一节说出了在声明method时使用指针作为接收器类型的两点原因:一是,可以直接修改pointer指向的值;二是,避免了方法调用时要先做备份,然后对备份修改时的性能损失(接收器类型为value时,使用value类型的变量对方法进行调用时,要先将value对象(或叫实例)进行copy,得到一个value对象副本,然后在副本上进行操作嘛,这样一来,如果value对象数据较大,则性能损失比较严重(量大复制粘贴的时间就长). 话又说回来了,各有优缺点, 使用value做为接收器时,对同一个变量多次调用时,前面的调用不会对后面的调用产生影响,因为每次在使用value类型对象调用相应的方法时都会先copy一个value类型对象的副本, 然后在该副本上进行引用与修改)
2.通常来说给某个作为接收器变量类型定义方法时,要么该接收器类型都是value,要么都是pointer,不建议部分方法的接收器类型为其value型,部分的为其pointer型.即如果有func (a A) x,则以后类型A只能以value类型作为方法的接收器, 不能出现func (a *A) y这种形式, 反之为然. //本节并没有说明为什么不能混用,说在下面几节能找到原因. 我写代码实际测了一下,编译这块并没有什么问题,一个是(a A),一个是(a *A)编译执行也都是没问题的. //看例子应该特指的就是某个类型实现接口中的多个方法时,接收器要么都是A,要么都是*A,不推荐有的方法是A,有的方法是*A,但下面有个链接也举例说了这个推荐是误导性的.无论最后结论什么样,当前下面第3点总结的这些行为先记住.
In general, all methods on a given type should have either value or pointer receivers,这句话里,on a given type这里的type应该就是特指的接口类型,all methods指的是接口中定义的N个方法, 后面either...or...中自然指的就是value和pointer接收器类型了
3.为了弄清上一条2所说的在编写代码时最好(同一个类)接收器的类型(value,pointer)不要混用,总结了一下:
/*
1.当未引入接口类型时,某个方法的接收器类型无论是value型还是pointer型,在调用的时候你都可以使用value型或pointer型进行调用(具体为啥上面已有详解).
2.当引入接口类型时,变数就来了, 这里要分为两种情况:
a.不显式的引入接口类型变量(或称为不将接口类型与接收器类型相关联), 此时可以忽略接口对方法进行调用,而且无论方法定义时用的接收器类型是value还是pointer,调用时还是上面讲的常规调用,即使用value或pointer都可以. //现在想来, 这一点和没说区别不大
type V struct {X,Y int}
type I interface {ok() int}
func (v V) ok() int{ //这里的接收器无论是V还是*V,由于主函数中没有引入接口类型I,而是直接调用的方法,所以,见下面注释
return 666
}
func main() {
//var i I