1 前言

1.1 Go汇编

Go语言被定义为一门系统编程语言,与C语言一样通过编译器生成可直接运行的二进制文件。这一点与Java,PHP,Python等编程语言存在很大的不同,这些语言都是运行在基于C语言开发的虚拟机上,如果想深入了解运行原理只需要看懂对应的C语言开发的虚拟机(绝大部分程序员应该都对C语言有基本的了解)。但是如果想深入学习Go语言,就需要对基本的汇编指令和语法有一定的了解(通过汇编可以了解到编译器到底做了什么工作)

 通过下面的例子简单了解如何通过汇编来了解Go语言的运行原理。编辑一个go文本call_function.go,输入如下代码:

     1  package main
2
3 func add(a, b int) int {
4 return a + b
5 }
6
7 func main() {
8 a := 10
9 b := 20
10
11 c := add(a, b)
12 _ = c
13 }

输入命令go build -gcflags '-l -N' call_function.go生成可执行文件,然后输入命令go tool objdump -s "main.main" call_function查看汇编代码如下:

     1  TEXT main.main(SB) /Users/didi/Source/Go/src/ppt/call_function.go
2 call_function.go:7 0x104f380 65488b0c25a0080000 MOVQ GS:0x8a0, CX
3 call_function.go:7 0x104f389 483b6110 CMPQ 0x10(CX), SP
4 call_function.go:7 0x104f38d 764c JBE 0x104f3db
5 call_function.go:7 0x104f38f 4883ec38 SUBQ $0x38, SP
6 call_function.go:7 0x104f393 48896c2430 MOVQ BP, 0x30(SP)
7 call_function.go:7 0x104f398 488d6c2430 LEAQ 0x30(SP), BP
8 call_function.go:8 0x104f39d 48c74424280a000000 MOVQ $0xa, 0x28(SP)
9 call_function.go:9 0x104f3a6 48c744242014000000 MOVQ $0x14, 0x20(SP)
10 call_function.go:11 0x104f3af 488b442428 MOVQ 0x28(SP), AX
11 call_function.go:11 0x104f3b4 48890424 MOVQ AX, 0(SP)
12 call_function.go:11 0x104f3b8 488b442420 MOVQ 0x20(SP), AX
13 call_function.go:11 0x104f3bd 4889442408 MOVQ AX, 0x8(SP)
14 call_function.go:11 0x104f3c2 e899ffffff CALL main.add(SB)
15 call_function.go:11 0x104f3c7 488b442410 MOVQ 0x10(SP), AX
16 call_function.go:11 0x104f3cc 4889442418 MOVQ AX, 0x18(SP)
17 call_function.go:13 0x104f3d1 488b6c2430 MOVQ 0x30(SP), BP
18 call_function.go:13 0x104f3d6 4883c438 ADDQ $0x38, SP
19 call_function.go:13 0x104f3da c3 RET
20 call_function.go:7 0x104f3db e89083ffff CALL runtime.morestack_noctxt(SB)
21 call_function.go:7 0x104f3e0 eb9e JMP main.main(SB)

第8~9行汇编代码,分别将SP(栈寄存器)偏移0x28和0x20的地址赋值为0xa和0x14,对应Go代码的第8行和第9行中的对a,b变量赋值,也就是说a变量对应的内存地址是SP+0x28,b变量对应的内存地址是SP+0x20。

 然后10~14行汇编代码表示对a,b变量进行拷贝,分别拷贝到SP+0x0和SP+0x8地址,然后调用add方法,这就是通常说到的函数调用时的“值传递”。

 输入命令go tool objdump -s "main.add" call_function,可以看到如下的汇编代码:

     1  TEXT main.add(SB) /Users/didi/Source/Go/src/ppt/call_function.go
2 call_function.go:3 0x104f360 48c744241800000000 MOVQ $0x0, 0x18(SP)
3 call_function.go:4 0x104f369 488b442408 MOVQ 0x8(SP), AX
4 call_function.go:4 0x104f36e 4803442410 ADDQ 0x10(SP), AX
5 call_function.go:4 0x104f373 4889442418 MOVQ AX, 0x18(SP)
6 call_function.go:4 0x104f378 c3 RET

第3~5行汇编代码表示,将SP+0x8和SP+0x10地址的值相加,并复制到SP+0x18地址。

 为什么在main函数中,a和b变量分别复制到了SP+0x0和SP+0x8地址,但是在add函数中,却将SP+0x8和SP+0x10地址的值进行相加呢?

 这是因为在main函数中的汇编代码14行中,调用call执行时CPU会执行一次压栈操作,将函数调用完成以后需要返回的地址存在SP-0x8的地址处,并执行一次SP=SP-0x8的操作(具体操作可以百度一下)。所以在add函数里面的SP+0x8和SP+0x10地址就对应着main函数中的SP+0x0和SP+0x8地址。

 具体过程如下图:

image

1.2 Go指针

Go的库代码中大量使用了一些指针进行内存操作。但是在Go语言中指针变量是不能进行运算的,所以不能像C语言那样方便的对内存进行偏移寻址,但是Go中提供了unsafe包来对指针计算运算。

 下面的例子可以说明使用方式:

     1  package main
2
3 import (
4 "fmt"
5 "unsafe"
6 )
7
8 type Struct1 struct {
9 A int64
10 B int64
11 C int64
12 }
13
14 type Struct2 struct {
15 A int64
16 B int64
17 C int64
18 }
19
20 func main() {
21 struct1 := Struct1 {
22 A : 1,
23 B : 2,
24 C : 3,
25 }
26
27 struct2 := new(Struct2)
28
29 var src uintptr = uintptr(unsafe.Pointer(&struct1))
30 var dst uintptr = uintptr(unsafe.Pointer(struct2))
31 for i := 0; i < 24; i++ {
32 *(*uint8)(unsafe.Pointer(dst + uintptr(i))) = *(*uint8)(unsafe.Pointer(src + uintptr(i)))
33 }
34
35 fmt.Println("struct1=%v||struct2=%v", struct1, *struct2);
36 }

在上面的例子将struct1对应内存的值复制到struct2对应的内存中,从例子中可以看出可以看到Go语言中

  • unsafe.Pointer类似于C中的void*,任何类型的指针都可以转换为unsafe.Pointer 类型,unsafe.Pointer 类型也可以转换为任何指针类型;
  • uintptr可以存go中的任何变量,如果想对指针进行运算,必须先把指针转换为uintptr。

2 Go的interface的实现

在Go语言中interface是一个非常重要的概念,也是与其它语言相比存在很大特色的地方。interface也是一个Go语言中的一种类型,是一种比较特殊的类型,存在两种interface,一种是带有方法的interface,一种是不带方法的interface。Go语言中的所有变量都可以赋值给空interface变量,实现了interface中定义方法的变量可以赋值给带方法的interface变量,并且可以通过interface直接调用对应的方法,实现了其它面向对象语言的多态的概念。

2.1 内部定义

两种不同的interface在Go语言内部被定义成如下的两种结构体(源码基于Go的1.9.2版本)

// 没有方法的interface
type eface struct {
_type *_type
data unsafe.Pointer
} // 记录着Go语言中某个数据类型的基本特征
type _type struct {
size uintptr
ptrdata uintptr
hash uint32
tflag tflag
align uint8
fieldalign uint8
kind uint8
alg *typeAlg
gcdata *byte
str nameOff
ptrToThis typeOff
} // 有方法的interface
type iface struct {
tab *itab
data unsafe.Pointer
} type itab struct {
inter *interfacetype
_type *_type
link *itab
hash uint32
bad bool
inhash bool
unused [2]byte
fun [1]uintptr
} // interface数据类型对应的type
type interfacetype struct {
typ _type
pkgpath name
mhdr []imethod
}

可以看到两种类型的interface在内部实现时都是定义成了一个2个字段的结构体,所以任何一个interface变量都是占用16个byte的内存空间。

在Go语言中_type这个结构体非常重要,记录着某种数据类型的一些基本特征,比如这个数据类型占用的内存大小(size字段),数据类型的名称(nameOff字段)等等。每种数据类型都存在一个与之对应的_type结构体(Go语言原生的各种数据类型,用户自定义的结构体,用户自定义的interface等等)。如果是一些比较特殊的数据类型,可能还会对_type结构体进行扩展,记录更多的信息,比如interface类型,就会存在一个interfacetype结构体,除了通用的_type外,还包含了另外两个字段pkgpath和mhdr,后文在对这两个字段的作用进行解析。除此之外还有其它类型的数据结构对应的结构体,比如structtype,chantype,slicetype,有兴趣的可以在$GOROOT/src/runtime/type.go文件中查看。

image

2.2 赋值

存在对没有方法的interface变量和有方法的interface变量赋值这两种不同的情况。分别详解这两种不同的赋值过程。

  • 没有方法的interface变量赋值

     对没有方法的interface变量赋值时编译器做了什么工作?创建一个eface.go文件,代码如下:
     1  package main
2
3 type Struct1 struct {
4 A int64
5 B int64
6 }
7
8 func main() {
9 s := new(Struct1)
10 var i interface{}
11 i = a
12
13 _ = i
14 }

输入命令go build -gcflags '-l -N' eface.go,go tool objdump -s "main.main" eface,查看汇编代码。

     1  TEXT main.main(SB) /Users/didi/Source/Go/src/ppt/eface.go
2 eface.go:8 0x104f360 4883ec38 SUBQ $0x38, SP
3 eface.go:8 0x104f364 48896c2430 MOVQ BP, 0x30(SP)
4 eface.go:8 0x104f369 488d6c2430 LEAQ 0x30(SP), BP
5 eface.go:9 0x104f36e 48c7042400000000 MOVQ $0x0, 0(SP)
6 eface.go:9 0x104f376 48c744240800000000 MOVQ $0x0, 0x8(SP)
7 eface.go:9 0x104f37f 488d0424 LEAQ 0(SP), AX
8 eface.go:9 0x104f383 4889442410 MOVQ AX, 0x10(SP)
9 eface.go:10 0x104f388 48c744242000000000 MOVQ $0x0, 0x20(SP)
10 eface.go:10 0x104f391 48c744242800000000 MOVQ $0x0, 0x28(SP)
11 eface.go:11 0x104f39a 488b442410 MOVQ 0x10(SP), AX
12 eface.go:11 0x104f39f 4889442418 MOVQ AX, 0x18(SP)
13 eface.go:11 0x104f3a4 488d0dd5670000 LEAQ 0x67d5(IP), CX
14 eface.go:11 0x104f3ab 48894c2420 MOVQ CX, 0x20(SP)
15 eface.go:11 0x104f3b0 4889442428 MOVQ AX, 0x28(SP)
16 eface.go:14 0x104f3b5 488b6c2430 MOVQ 0x30(SP), BP
17 eface.go:14 0x104f3ba 4883c438 ADDQ $0x38, SP

汇编代码第56行给结构体Struct1分配了空间SP+0x0和SP+0x8,第78行把这个结构体的地址放在存入了SP+0x10地址,这个地址就是变量s,第910行给interface类型的变量i分配了SP+0x20和SP+0x28,第1314行把结构体A对应的_type的地址赋值到SP+0x20,然后把a变量赋值到了SP+0x28。这就是对没有方法的interface进行赋值的过程。赋值完以后的内存分配如下图:

image

  • 有方法的interface变量赋值

     如下一段代码在内存的分布
     1  package main
2
3 type I interface {
4 Add()
5 Del()
6 }
7
8 type Struct1 struct {
9 A int64
10 B int64
11 }
12
13 func (a *Struct1) Add() {
14 a.A = a.A + 1
15 a.B = a.B + 1
16 }
17
18 func (a *Struct1) Del() {
19 a.A = a.A - 1
20 a.B = a.B - 1
21 }
22
23 func main() {
24 a := new(Struct1)
25 var i I
26 i = a
27
28 i.Add()
29 i.Del()
30 }

image

这些内存地址都可以使用gdb调试时得到

(gdb) p i
$11 = {tab = 0x10a70e0 <Struct1,main.I>, data = 0xc42001a0c0}
(gdb) p a
$12 = (struct main.Struct1 *) 0xc42001a0c0
(gdb) p i.tab
$13 = (runtime.itab *) 0x10a70e0 <Struct1,main.I>
(gdb) p i.tab.inter
$14 = (runtime.interfacetype *) 0x105dc60 <type.*+59232>
(gdb) p i.tab._type
$15 = (runtime._type *) 0x105d200 <type.*+56576>

通过对内存地址的打印,可以很清晰的看出在对有方法的interface变量进行赋值时的内存分布。Struct1类型和interface I类型都存在内存记录着各自的_type结构体信息,在将Struct1类型的变量赋值给interface I类型时,会有一个itab类型的结构体将Struct1类型和interface I类型关联起来。

上面的例子都是将一个指针赋值给interface变量,如果是将一个值赋值给interface变量。会先对分配一块空间保存该值的副本,然后将该interface变量的data字段指向这个新分配的空间。将一个值赋值给interface变量时,操作的都是该值的一个副本。

2.3 方法的调用

上面对有方法的interface进行赋值后,是如何实现通过接口变量实现了函数调用呢?参考下面的汇编代码

     1  TEXT main.main(SB) /Users/didi/Source/Go/src/ppt/iface.go
2 iface.go:23 0x104f3e0 65488b0c25a0080000 MOVQ GS:0x8a0, CX
3 iface.go:23 0x104f3e9 483b6110 CMPQ 0x10(CX), SP
4 iface.go:23 0x104f3ed 0f8687000000 JBE 0x104f47a
5 iface.go:23 0x104f3f3 4883ec38 SUBQ $0x38, SP
6 iface.go:23 0x104f3f7 48896c2430 MOVQ BP, 0x30(SP)
7 iface.go:23 0x104f3fc 488d6c2430 LEAQ 0x30(SP), BP
8 iface.go:23 0x104f401 488d0578ff0000 LEAQ 0xff78(IP), AX
9 iface.go:24 0x104f408 48890424 MOVQ AX, 0(SP)
10 iface.go:24 0x104f40c e86fcefbff CALL runtime.newobject(SB)
11 iface.go:24 0x104f411 488b442408 MOVQ 0x8(SP), AX
12 iface.go:24 0x104f416 4889442410 MOVQ AX, 0x10(SP)
13 iface.go:25 0x104f41b 48c744242000000000 MOVQ $0x0, 0x20(SP)
14 iface.go:25 0x104f424 48c744242800000000 MOVQ $0x0, 0x28(SP)
15 iface.go:26 0x104f42d 488b442410 MOVQ 0x10(SP), AX
16 iface.go:26 0x104f432 4889442418 MOVQ AX, 0x18(SP)
17 iface.go:26 0x104f437 488d0da27c0500 LEAQ 0x57ca2(IP), CX
18 iface.go:26 0x104f43e 48894c2420 MOVQ CX, 0x20(SP)
19 iface.go:26 0x104f443 4889442428 MOVQ AX, 0x28(SP)
20 iface.go:28 0x104f448 488b442420 MOVQ 0x20(SP), AX
21 iface.go:28 0x104f44d 488b4020 MOVQ 0x20(AX), AX
22 iface.go:28 0x104f451 488b4c2428 MOVQ 0x28(SP), CX
23 iface.go:28 0x104f456 48890c24 MOVQ CX, 0(SP)
24 iface.go:28 0x104f45a ffd0 CALL AX
25 iface.go:29 0x104f45c 488b442420 MOVQ 0x20(SP), AX
26 iface.go:29 0x104f461 488b4028 MOVQ 0x28(AX), AX
27 iface.go:29 0x104f465 488b4c2428 MOVQ 0x28(SP), CX
28 iface.go:29 0x104f46a 48890c24 MOVQ CX, 0(SP)
29 iface.go:29 0x104f46e ffd0 CALL AX
30 iface.go:30 0x104f470 488b6c2430 MOVQ 0x30(SP), BP
31 iface.go:30 0x104f475 4883c438 ADDQ $0x38, SP
32 iface.go:30 0x104f479 c3 RET
33 iface.go:23 0x104f47a e8f182ffff CALL runtime.morestack_noctxt(SB)
34 iface.go:23 0x104f47f e95cffffff JMP main.main(SB)

汇编代码的第17行和18行,将itab的地址加载到SP+0x20地址处,第20,21行,24行将SP+0x20的值加载到AX寄存器,然后将AX+0x20地址的值加载到AX寄存器,CALL AX就实现了add方法的调用,其中第22行和23行的作用是将interface里面data字段的地址传递给了add方法。

image

通过对itab结构体进行分析,可以看到偏移0x20处为fun字段,其中0x20处为add函数的入口地址,0x28处就是del函数的入口地址。

2.4 断言的实现

在Go语言中,经常需要对一个interface变量进行断言

     1  package main
2
3 type Struct1 struct {
4 A int64
5 }
6
7 func main() {
8 a := new(Struct1)
9
10 var i interface{}
11 i = a
12
13 b, ok := i.(Struct1)
14 if ok {
15 _ = b
16 }
17 }

生成汇编代码进行分析

     1  TEXT main.main(SB) /Users/didi/Source/Go/src/ppt/assert.go
2 assert.go:7 0x104f360 4883ec48 SUBQ $0x48, SP
3 assert.go:7 0x104f364 48896c2440 MOVQ BP, 0x40(SP)
4 assert.go:7 0x104f369 488d6c2440 LEAQ 0x40(SP), BP
5 assert.go:8 0x104f36e 48c744241000000000 MOVQ $0x0, 0x10(SP)
6 assert.go:8 0x104f377 488d442410 LEAQ 0x10(SP), AX
7 assert.go:8 0x104f37c 4889442420 MOVQ AX, 0x20(SP)
8 assert.go:10 0x104f381 48c744243000000000 MOVQ $0x0, 0x30(SP)
9 assert.go:10 0x104f38a 48c744243800000000 MOVQ $0x0, 0x38(SP)
10 assert.go:11 0x104f393 488b442420 MOVQ 0x20(SP), AX
11 assert.go:11 0x104f398 4889442428 MOVQ AX, 0x28(SP)
12 assert.go:11 0x104f39d 488d0d1c680000 LEAQ 0x681c(IP), CX
13 assert.go:11 0x104f3a4 48894c2430 MOVQ CX, 0x30(SP)
14 assert.go:11 0x104f3a9 4889442438 MOVQ AX, 0x38(SP)
15 assert.go:13 0x104f3ae 488b442438 MOVQ 0x38(SP), AX
16 assert.go:13 0x104f3b3 488b4c2430 MOVQ 0x30(SP), CX
17 assert.go:13 0x104f3b8 488d1581ed0000 LEAQ 0xed81(IP), DX
18 assert.go:13 0x104f3bf 4839d1 CMPQ DX, CX
19 assert.go:13 0x104f3c2 7402 JE 0x104f3c6
20 assert.go:13 0x104f3c4 eb3f JMP 0x104f405
21 assert.go:13 0x104f3c6 488b00 MOVQ 0(AX), AX
22 assert.go:13 0x104f3c9 b901000000 MOVL $0x1, CX
23 assert.go:13 0x104f3ce eb00 JMP 0x104f3d0

汇编的第12行,17行,18行可以看出,将Struct1对应的_type结构体的地址赋值给interface以后。在进行断言的时候,原理就是将interface变量_type字段的与Struct1对应的_type结构地址进行对比。

在本例子中,第12行的IP寄存器对应的值是0x104f39d,0x681c(IP)对应的地址为0x1055BB9,第17行的IP寄存器对应的值是0x104f3b8,0xed81(IP)对应的地址为0x105E139,貌似并不相同。可能是对Go的汇编中对IP寄存器的理解存在偏差,找了几个小时资料都没找到原因。

3 Go的反射

反射是一种强大的语言特性,可以“动态”的调用方法,获取结构体运行时的一些特征,很多框架的实现都离不开反射。Go的反射就是通过interface类型来实现的。

3.1 反射获取变量的信息

Go的反射包主要存在两个重要的结构体。

     1  type Value struct {
2 typ *rtype
3 ptr unsafe.Pointer
4 flag
5 }
6
7 func ValueOf(i interface{}) Value {
8 }
9
10 type Type interface {
11 Align() int
12 FieldAlign() int
13 Method(int) Method
14 Name() string
15 //一堆方法
16 //....
17 }
18
19 func TypeOf(i interface{}) Type {
20 eface := *(*emptyInterface)(unsafe.Pointer(&i))
21 return toType(eface.typ)
22 }
23
24 type emptyInterface struct {
25 typ *rtype
26 word unsafe.Pointer
27 }

任何一个变量可以通过调用ValueOf来获取到变量的Value结构体,通过TypeOf方法来获取变量的Type接口类型。通过TypeOf方法获取到的Type接口实际上就是该变量对应的_type。

 通过前面的分析,当通过TypeOf方法获取到变量的_type结构体后,很容易获取到该变量的一些基本信息,比如_type结构体中的各种字段都可以直接获取到。

3.2 反射修改变量的值

     1  package main
2
3 import (
4 "reflect"
5 )
6
7 func main() {
8 var x int64 = 10
9
10 reflect.ValueOf(x).SetInt(20)
11
12 reflect.ValueOf(&x).SetInt(20)
13
14 reflect.ValueOf(&x).Elem().SetInt(20)
15 }

上面的例子中,第10行,12行都会报panic,只有第14行能修改变量的值。在使用ValueOf获取到Value结构体以后,flag字段记录着值能否进行修改,这样应该是为了避免误操作,保证api调用者明确了解到是否需要修改值。

3.3 反射修改结构体变量字段的值

如果需要通过反射修改某结构体里面各个字段的值。

     1  package main
2
3 import (
4 "reflect"
5 "fmt"
6 )
7
8 type Struct1 struct {
9 A int64
10 B int64
11 C int64
12 }
13
14 func main() {
15 P := new(Struct1)
16
17 V := reflect.ValueOf(P).Elem()
18 V.FieldByName("A").SetInt(100)
19 V.FieldByName("B").SetInt(200)
20 V.FieldByName("C").SetInt(300)
21
22 fmt.Printf("%v", P)
23 }

上面的代码中,需要根据结构体字段的名称对各个字段的值进行修改,内部是如何实现的呢?

image

每一个自定义的struct类型都存在这一个对应的structType结构体,该结构体记录了每个字段structField。通过对比structField里面的name字段,就可以获取到某个字段的type和偏移量。从而对具体的值进行修改。

3.4 反射动态调用方法

动态的调用方法是怎么实现的?

     1  package main
2
3 import (
4 "reflect"
5 )
6
7 type Struct1 struct {
8 A int64
9 B int64
10 C int64
11 }
12
13 func (p *Struct1) Set() {
14 p.A = 200
15 }
16
17 func main() {
18 P := new(Struct1)
19 P.A = 100
20 P.B = 200
21 P.C = 300
22
23 V := reflect.ValueOf(P)
24
25 params := make([]reflect.Value, 0)
26 V.MethodByName("Set").Call(params)
27 }

结构体的方法在内存中存在如下的分布

image

在编译过程中,结构体对应方法的相关信息都已经存在于内存中,分配了一块uncommonType的结构体跟在fields字段后面。根据内存的分布,如果需要根据一个结构体的名称获取到方法并且执行,只需要根据uncommonType结构中的moff字段去获取方法相关信息的地址块,然后逐个对比名称是否为想要获取的方法进行调用。

4 总结

本文从实现原理上分析了Go语言中interface类型和反射包的使用,相信各位读者以后再使用Go的interface类型和反射包时能做到胸有成竹,也能够对分析Go语言的其它特性提供思路。

作者:喻家山车神

链接:https://www.jianshu.com/p/70003e0f49d1

来源:简书

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

go的接口内部实现的更多相关文章

  1. golang(09) golang 接口内部实现

    原文链接 http://www.limerence2017.com/2019/09/24/golang14/#more 前文介绍过golang interface用法,本文详细剖析interface内 ...

  2. Go语言接口内部布局和方法集详解

    1. 接口值内部布局   如果用户定义的类型实现了某个接口类型声明的一组方法,那么这个用户定义的类型的值就可以赋给这个接口类型的值.这个赋值会把用户定义的类型的值存入接口类型的值.赋值完成后得到的值称 ...

  3. Java中的内部接口

    什么是内部接口 内部接口也称为嵌套接口,即在一个接口内部定义另一个接口.举个例子,Entry接口定义在Map接口里面,如下代码: public interface Map { interface En ...

  4. Java:内部接口

    1.什么是内部接口 内部接口也称为嵌套接口,即在一个接口内部定义另一个接口.举个例子,Entry接口定义在Map接口里面,如下代码: public interface Map { interface ...

  5. web api接口同步和异步的问题

    一般来说,如果一个api 接口带上Task和 async 一般就算得上是异步api接口了. 如果我想使用异步api接口,一般的动机是我在我的方法里面可能使用Task.Run 进行异步的去处理一个耗时的 ...

  6. Golang接口(interface)三个特性(译文)

    The Laws of Reflection 原文地址 第一次翻译文章,请各路人士多多指教! 类型和接口 因为映射建设在类型的基础之上,首先我们对类型进行全新的介绍. go是一个静态性语言,每个变量都 ...

  7. ArcEngine 岛状多边形内部环的获取

    ArcEngine岛状多边形获取其内部环 查阅了帮助文档相关接口,内部环的获方法get_InteriorRingBag() 需要外部环作为参数.而外部环可以直接通过ExteriorRingBag属性获 ...

  8. 优秀的API接口设计原则及方法(转)

    一旦API发生变化,就可能对相关的调用者带来巨大的代价,用户需要排查所有调用的代码,需要调整所有与之相关的部分,这些工作对他们来说都是额外的.如果辛辛苦苦完成这些以后,还发现了相关的bug,那对用户的 ...

  9. C# 接口应用及意义

    写在前面:新手入行,读者勉强看看吧,写的不对的欢迎讨论,板砖轻拍! 一.定义 接口描述的是可属于任何类或结构的一组相关功能,所以实现接口的类或结构必须实现接口定义中指定的接口成员. 通常用Interf ...

随机推荐

  1. JDBC简单增删改查实现(单表)

    0.准备工作 开发工具: MySQL数据库, intelliJ IDEA2017. 准备jar包: mysql-connector-java-5.1.28-bin.jar(其他均可) 1. 数据库数据 ...

  2. Java之路---Day07

    2019-10-21-23:30:24 ArrayList类[集合] What:java.util.ArrayList是大小可变的数组的实现,存储在内的数据称为元元素,此类提供一些方法来操作内部存储的 ...

  3. Ajax实现异步请求

    基本步骤:创建XMLHttpRequest对象-->配置发送参数-->执行发送-->处理响应 ajax 通俗讲有四个步骤 1.创建Ajax对象2.链接到服务器3.发送请求4.接受返回 ...

  4. 冠捷显示成功的信息化建设(MES应用案例)

    企业介绍 冠捷科技集团是驰誉全球的大型高科技跨国企业,产品包括彩色显示器( CRT monitor ).液晶显示器( LCD monitor ).液晶电视( LCD-TV )与等离子电视( PDP ) ...

  5. 使用FMXlinux 开发linux 桌面应用

    自从delphi 10.2 开始正式支持linux  开发来,大家一直关心为什么官方没有使用FMX来支持LInux 的桌面开发? 其实原因无非就几点: 1.Linux 大部分是应用还是服务端的,桌面应 ...

  6. Mysql 整数类型的字段的属性设置及常用的函数

    数据类型 二.MySQL支持的数据类型 数值类型.日期类型.字符串类型 1.数值类型 1)整数类型 tinyint.smallint.mediumint.int和bigint 2)zerofill属性 ...

  7. 设置Linux 程序lib搜索目录

    设置Linux 程序lib搜索目录:export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:lib路径,例如: export LD_LIBRARY_PATH=$LD_LIBRA ...

  8. C++(四十八) — string容器的基本操作

    参考博客:https://blog.csdn.net/qq_37941471/article/details/82107077 https://www.cnblogs.com/danielStudy/ ...

  9. 【转】GnuPG使用介绍

    一.什么是 GPG 要了解什么是 GPG,就要先了解 PGP. 1991 年,程序员 Phil Zimmermann 为了避开政府监视,开发了加密软件 PGP.这个软件非常好用,迅速流传开来,成了许多 ...

  10. tkinter代码正式版

    可以绘图了. import json import tkinter as tk from tkinter import filedialog from tkinter import LabelFram ...