go中的关键字-defer
1. defer的使用
defer 延迟调用。我们先来看一下,有defer关键字的代码执行顺序:
func main() {
defer func() {
fmt.Println("1号输出")
}()
defer func() {
fmt.Println("2号输出")
}()
}
输出结果:
2号出来
1号出来
结论:多个defer的执行顺序是倒序执行(同入栈先进后出)。
由例子可以看出来,defer有延迟生效的作用,先使用defer的语句延迟到最后执行。
1.1 defer与返回值之间的顺序
func defertest() int func main() {
fmt.Println("main:", defertest())
} func defertest() int {
var i int
defer func() {
i++
fmt.Println("defer2的值:", i)
}()
defer func() {
i++
fmt.Println("defer1的值:", i)
}()
return i
}
输出结果:
defer1的值:
defer2的值:
main:
结论:return最先执行->return负责将结果写入返回值中->接着defer开始执行一些收尾工作->最后函数携带当前返回值退出
return的时候已经先将返回值给定义下来了,就是0,由于i是在函数内部声明所以即使在defer中进行了++操作,也不会影响return的时候做的决定。
func test() (i int) func main() {
fmt.Println("main:", test())
} func test() (i int) {
defer func() {
i++
fmt.Println("defer2的值:", i)
}()
defer func() {
i++
fmt.Println("defer1的值:", i)
}()
return i
}
详解:由于返回值提前声明了,所以在return的时候决定的返回值还是0,但是后面两个defer执行后进行了两次++,将i的值变为2,待defer执行完后,函数将i值进行了返回。
2. defer定义和执行
func test(i *int) int {
return *i
} func main(){
var i = // defer定义的时候test(&i)的值就已经定了,是1,后面就不会变了
defer fmt.Println("i1 =" , test(&i))
i++ // defer定义的时候test(&i)的值就已经定了,是2,后面就不会变了
defer fmt.Println("i2 =" , test(&i)) // defer定义的时候,i就已经确定了是一个指针类型,地址上的值变了,这里跟着变
defer func(i *int) {
fmt.Println("i3 =" , *i)
}(&i) // defer定义的时候i的值就已经定了,是2,后面就不会变了
defer func(i int) {
//defer 在定义的时候就定了
fmt.Println("i4 =" , i)
}(i) defer func() {
// 地址,所以后续跟着变
var c = &i
fmt.Println("i5 =" , *c)
}() // 执行了 i=11 后才调用,此时i值已是11
defer func() {
fmt.Println("i6 =" , i)
}() i =
}
结论:会先将defer后函数的参数部分的值(或者地址)给先下来【你可以理解为()里头的会先确定】,后面函数执行完,才会执行defer后函数的{}中的逻辑。
例题分析
//例子1
func f() (result int) {
defer func() {
result++
}()
return
}
//例子2
func f() (r int) {
t :=
defer func() {
t = t +
}()
return t
}
//例子3
func f() (r int) {
defer func(r int) {
r = r +
}(r)
return
}
例1的正确答案不是0,例2的正确答案不是10,例3的正确答案不是6......
这里先说一下返回值。defer是在return之前执行的。这条规则毋庸置疑,但最重要的一点是要明白,return xxx这一条语句并不是一条原子指令!
函数返回的过程:先给返回值赋值,然后调用defer表达式,最后才是返回到调用函数中。defer表达式可能会在设置函数返回值之后,且在返回到调用函数之前去修改返回值,使最终的函数返回值与你想象的不一致。
return xxx 可被改写成:
返回值 = xxx
调用defer函数
空的return
所以例子也可以改写成:
//例1
func f() (result int) {
result = //return语句不是一条原子调用,return xxx其实是赋值+ret指令
func() { //defer被插入到return之前执行,也就是赋返回值和ret指令之间
result++
}()
return
}
//例2
func f() (r int) {
t :=
r = t //赋值指令
func() { //defer被插入到赋值与返回之间执行,这个例子中返回值r没被修改过
t = t +
}
return //空的return指令
}
例3
func f() (r int) {
r = //给返回值赋值
func(r int) { //这里改的r是传值传进去的r,不会改变要返回的那个r值
r = r +
}(r)
return //空的return
}
所以例1的结果是1,例2的结果是5,例3的结果是1.
3. defer内部原理
从例子开始看:
packmage main import() func main() {
defer println("这是一个测试")
}
反编译一下看看:
➜ src $ go build -o test test.go
➜ src $ go tool objdump -s "main\.main" test
TEXT main.main(SB) /Users/tushanshan/go/src/test3.go
test3.go: 0x104ea70 65488b0c2530000000 MOVQ GS:0x30, CX
test3.go: 0x104ea79 483b6110 CMPQ 0x10(CX), SP
test3.go: 0x104ea7d 765f JBE 0x104eade
test3.go: 0x104ea7f 4883ec28 SUBQ $0x28, SP
test3.go: 0x104ea83 48896c2420 MOVQ BP, 0x20(SP)
test3.go: 0x104ea88 488d6c2420 LEAQ 0x20(SP), BP
test3.go: 0x104ea8d c7042410000000 MOVL $0x10, (SP)
test3.go: 0x104ea94 488d05e5290200 LEAQ go.func.*+(SB), AX
test3.go: 0x104ea9b MOVQ AX, 0x8(SP)
test3.go: 0x104eaa0 488d05e6e50100 LEAQ go.string.*+(SB), AX
test3.go: 0x104eaa7 MOVQ AX, 0x10(SP)
test3.go: 0x104eaac 48c744241804000000 MOVQ $0x4, 0x18(SP)
test3.go: 0x104eab5 e8b631fdff CALL runtime.deferproc(SB)
test3.go: 0x104eaba 85c0 TESTL AX, AX
test3.go: 0x104eabc JNE 0x104eace
test3.go: 0x104eabe NOPL
test3.go: 0x104eabf e83c3afdff CALL runtime.deferreturn(SB)
test3.go: 0x104eac4 488b6c2420 MOVQ 0x20(SP), BP
test3.go: 0x104eac9 4883c428 ADDQ $0x28, SP
test3.go: 0x104eacd c3 RET
test3.go: 0x104eace NOPL
test3.go: 0x104eacf e82c3afdff CALL runtime.deferreturn(SB)
test3.go: 0x104ead4 488b6c2420 MOVQ 0x20(SP), BP
test3.go: 0x104ead9 4883c428 ADDQ $0x28, SP
test3.go: 0x104eadd c3 RET
test3.go: 0x104eade e8cd84ffff CALL runtime.morestack_noctxt(SB)
test3.go: 0x104eae3 eb8b JMP main.main(SB)
:- 0x104eae5 cc INT $0x3
:- 0x104eae6 cc INT $0x3
:- 0x104eae7 cc INT $0x3
编译器将defer处理成两个函数调用 deferproc 定义一个延迟调用对象,然后在函数结束前通过 deferreturn 完成最终调用。在defer出现的地方,插入了指令call runtime.deferproc,然后在函数返回之前的地方,插入指令call runtime.deferreturn。
内部结构
//defer
type _defer struct {
siz int32 // 参数的大小
started bool // 是否执行过了
sp uintptr // sp at time of defer
pc uintptr
fn *funcval
_panic *_panic // defer中的panic
link *_defer // defer链表,函数执行流程中的defer,会通过 link这个 属性进行串联
}
//panic
type _panic struct {
argp unsafe.Pointer // pointer to arguments of deferred call run during panic; cannot move - known to liblink
arg interface{} // argument to panic
link *_panic // link to earlier panic
recovered bool // whether this panic is over
aborted bool // the panic was aborted
}
//g
type g struct {
_panic *_panic // panic组成的链表
_defer *_defer // defer组成的先进后出的链表,同栈
}
因为 defer panic 都是绑定在运行的g上的,这里也说一下g中与 defer panic相关的属性
再把defer, panic, recover放一起看一下:
func main() {
defer func() {
recover()
}()
panic("error")
}
反编译结果:
go build -gcflags=all="-N -l" main.go
go tool objdump -s "main.main" main
go tool objdump -s "main\.main" main | grep CALL
main.go: 0x4548d0 e81b00fdff CALL runtime.deferproc(SB)
main.go: 0x4548f2 e8b90cfdff CALL runtime.gopanic(SB)
main.go: 0x4548fa e88108fdff CALL runtime.deferreturn(SB)
main.go: 0x454909 e85282ffff CALL runtime.morestack_noctxt(SB)
main.go: 0x4549a6 e8d511fdff CALL runtime.gorecover(SB)
main.go: 0x4549b5 e8a681ffff CALL runtime.morestack_noctxt(SB)
defer
关键字首先会调用 runtime.deferproc
定义一个延迟调用对象,然后再函数结束前,调用 runtime.deferreturn
来完成 defer
定义的函数的调用
panic
函数就会调用 runtime.gopanic
来实现相关的逻辑
recover
则调用 runtime.gorecover
来实现 recover 的功能
deferproc
根据 defer 关键字后面定义的函数 fn 以及 参数的size,来创建一个延迟执行的 函数,并将这个延迟函数,挂在到当前g的 _defer 的链表上,下面是deferproc的实现:
func deferproc(siz int32, fn *funcval) { // arguments of fn follow fn
sp := getcallersp()
argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
callerpc := getcallerpc()
// 获取一个_defer对象, 并放入g._defer链表的头部
d := newdefer(siz)
// 设置defer的fn pc sp等,后面调用
d.fn = fn
d.pc = callerpc
d.sp = sp
switch siz {
case :
// Do nothing.
case sys.PtrSize:
// _defer 后面的内存 存储 argp的地址信息
*(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp))
default:
// 如果不是指针类型的参数,把参数拷贝到 _defer 的后面的内存空间
memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz))
}
return0()
}
通过newproc
获取一个 _defer 的对象,并加入到当前g的 _defer 链表的头部,然后再把参数或参数的指针拷贝到 获取到的 _defer对象的后面的内存空间。
再看看newdefer
的实现:
func newdefer(siz int32) *_defer {
var d *_defer
// 根据 size 通过deferclass判断应该分配的 sizeclass,就类似于 内存分配预先确定好几个sizeclass,然后根据size确定sizeclass,找对应的缓存的内存块
sc := deferclass(uintptr(siz))
gp := getg()
// 如果sizeclass在既定的sizeclass范围内,去g绑定的p上找
if sc < uintptr(len(p{}.deferpool)) {
pp := gp.m.p.ptr()
if len(pp.deferpool[sc]) == && sched.deferpool[sc] != nil {
// 当前sizeclass的缓存数量==0,且不为nil,从sched上获取一批缓存
systemstack(func() {
lock(&sched.deferlock)
for len(pp.deferpool[sc]) < cap(pp.deferpool[sc])/ && sched.deferpool[sc] != nil {
d := sched.deferpool[sc]
sched.deferpool[sc] = d.link
d.link = nil
pp.deferpool[sc] = append(pp.deferpool[sc], d)
}
unlock(&sched.deferlock)
})
}
// 如果从sched获取之后,sizeclass对应的缓存不为空,分配
if n := len(pp.deferpool[sc]); n > {
d = pp.deferpool[sc][n-]
pp.deferpool[sc][n-] = nil
pp.deferpool[sc] = pp.deferpool[sc][:n-]
}
}
// p和sched都没有找到 或者 没有对应的sizeclass,直接分配
if d == nil {
// Allocate new defer+args.
systemstack(func() {
total := roundupsize(totaldefersize(uintptr(siz)))
d = (*_defer)(mallocgc(total, deferType, true))
})
}
d.siz = siz
// 插入到g._defer的链表头
d.link = gp._defer
gp._defer = d
return d
}
newdefer的作用是获取一个_defer对象, 并推入 g._defer链表的头部。根据size获取sizeclass,对sizeclass进行分类缓存,这是内存分配时的思想,先去p上分配,然后批量从全局 sched上获取到本地缓存,这种二级缓存的思想真的在go源码的各个部分都有。
deferreturn
func deferreturn(arg0 uintptr) {
gp := getg()
// 获取g defer链表的第一个defer,也是最后一个声明的defer
d := gp._defer
// 没有defer,就不需要干什么事了
if d == nil {
return
}
sp := getcallersp()
// 如果defer的sp与callersp不匹配,说明defer不对应,有可能是调用了其他栈帧的延迟函数
if d.sp != sp {
return
}
// 根据d.siz,把原先存储的参数信息获取并存储到arg0里面
switch d.siz {
case :
// Do nothing.
case sys.PtrSize:
*(*uintptr)(unsafe.Pointer(&arg0)) = *(*uintptr)(deferArgs(d))
default:
memmove(unsafe.Pointer(&arg0), deferArgs(d), uintptr(d.siz))
}
fn := d.fn
d.fn = nil
// defer用过了就释放了,
gp._defer = d.link
freedefer(d)
// 跳转到执行defer
jmpdefer(fn, uintptr(unsafe.Pointer(&arg0)))
}
freedefer
释放defer用到的函数,应该跟调度器、内存分配的思想是一样的。
func freedefer(d *_defer) {
// 判断defer的sizeclass
sc := deferclass(uintptr(d.siz))
// 超出既定的sizeclass范围的话,就是直接分配的内存,那就不管了
if sc >= uintptr(len(p{}.deferpool)) {
return
}
pp := getg().m.p.ptr()
// p本地sizeclass对应的缓冲区满了,批量转移一半到全局sched
if len(pp.deferpool[sc]) == cap(pp.deferpool[sc]) {
// 使用g0来转移
systemstack(func() {
var first, last *_defer
for len(pp.deferpool[sc]) > cap(pp.deferpool[sc])/ {
n := len(pp.deferpool[sc])
d := pp.deferpool[sc][n-]
pp.deferpool[sc][n-] = nil
pp.deferpool[sc] = pp.deferpool[sc][:n-]
// 先将需要转移的那批defer对象串成一个链表
if first == nil {
first = d
} else {
last.link = d
}
last = d
}
lock(&sched.deferlock)
// 把这个链表放到sched.deferpool对应sizeclass的链表头
last.link = sched.deferpool[sc]
sched.deferpool[sc] = first
unlock(&sched.deferlock)
})
}
// 清空当前要释放的defer的属性
d.siz =
d.started = false
d.sp =
d.pc =
d.link = nil pp.deferpool[sc] = append(pp.deferpool[sc], d)
}
gopanic
func gopanic(e interface{}) {
gp := getg() var p _panic
p.arg = e
p.link = gp._panic
gp._panic = (*_panic)(noescape(unsafe.Pointer(&p))) atomic.Xadd(&runningPanicDefers, )
// 依次执行 g._defer链表的defer对象
for {
d := gp._defer
if d == nil {
break
} // If defer was started by earlier panic or Goexit (and, since we're back here, that triggered a new panic),
// take defer off list. The earlier panic or Goexit will not continue running.
// 正常情况下,defer执行完成之后都会被移除,既然这个defer没有移除,原因只有两种: 1. 这个defer里面引发了panic 2. 这个defer里面引发了 runtime.Goexit,但是这个defer已经执行过了,需要移除,如果引发这个defer没有被移除是第一个原因,那么这个panic也需要移除,因为这个panic也执行过了,这里给panic增加标志位,以待后续移除
if d.started {
if d._panic != nil {
d._panic.aborted = true
}
d._panic = nil
d.fn = nil
gp._defer = d.link
freedefer(d)
continue
}
d.started = true // Record the panic that is running the defer.
// If there is a new panic during the deferred call, that panic
// will find d in the list and will mark d._panic (this panic) aborted.
// 把当前的panic 绑定到这个defer上面,defer里面有可能panic,这种情况下就会进入到 上面d.started 的逻辑里面,然后把当前的panic终止掉,因为已经执行过了
d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
// 执行defer.fn
p.argp = unsafe.Pointer(getargp())
reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))
p.argp = nil // reflectcall did not panic. Remove d.
if gp._defer != d {
throw("bad defer entry in panic")
}
// 解决defer与panic的绑定关系,因为 defer函数已经执行完了,如果有panic或Goexit就不会执行到这里了
d._panic = nil
d.fn = nil
gp._defer = d.link // trigger shrinkage to test stack copy. See stack_test.go:TestStackPanic
//GC() pc := d.pc
sp := unsafe.Pointer(d.sp) // must be pointer so it gets adjusted during stack copy
freedefer(d)
// panic被recover了,就不需要继续panic了,继续执行剩余的代码
if p.recovered {
atomic.Xadd(&runningPanicDefers, -) gp._panic = p.link
// Aborted panics are marked but remain on the g.panic list.
// Remove them from the list.
// 从panic链表中移除aborted的panic,下面解释
for gp._panic != nil && gp._panic.aborted {
gp._panic = gp._panic.link
}
if gp._panic == nil { // must be done with signal
gp.sig =
}
// Pass information about recovering frame to recovery.
gp.sigcode0 = uintptr(sp)
gp.sigcode1 = pc
// 调用recovery, 恢复当前g的调度执行
mcall(recovery)
throw("recovery failed") // mcall should not return
}
}
// 打印panic信息
preprintpanics(gp._panic)
// panic
fatalpanic(gp._panic) // should not return
*(*int)(nil) = // not reached
84 }
看下里面gp._panic.aborted
的作用:
func main() {
defer func() { // defer1
recover()
}()
panic1()
} func panic1() {
defer func() { // defer2
panic("error1") // panic2
}()
panic("error") // panic1
}
执行顺序详解:
- 当执行到
panic("error")
时
g._defer链表: g._defer->defer2->defer1
g._panic链表:g._panic->panic1
- 当执行到
panic("error1")
时
g._defer链表: g._defer->defer2->defer1
g._panic链表:g._panic->panic2->panic1
- 继续执行到 defer1 函数内部,进行recover()
此时会去恢复 panic2 引起的 panic, panic2.recovered = true,应该顺着g._panic链表继续处理下一个panic了,但是我们可以发现panic1
已经执行过了,这也就是下面的代码的逻辑了,去掉已经执行过的panic
for gp._panic != nil && gp._panic.aborted {
gp._panic = gp._panic.link
}
panic的逻辑:
程序在遇到panic的时候,就不再继续执行下去了,先把当前panic
挂载到 g._panic
链表上,开始遍历当前g的g._defer
链表,然后执行_defer
对象定义的函数等,如果 defer函数在调用过程中又发生了 panic,则又执行到了 gopanic
函数,最后,循环打印所有panic的信息,并退出当前g。然而,如果调用defer的过程中,遇到了recover,则继续进行调度(mcall(recovery))。
recovery
func recovery(gp *g) {
// Info about defer passed in G struct.
sp := gp.sigcode0
pc := gp.sigcode1
// Make the deferproc for this d return again,
// this time returning 1. The calling function will
// jump to the standard return epilogue.
// 记录defer返回的sp pc
gp.sched.sp = sp
gp.sched.pc = pc
gp.sched.lr =
gp.sched.ret =
// 重新恢复执行调度
gogo(&gp.sched)
}
gorecover
gorecovery
仅仅只是设置了 g._panic.recovered
的标志位
func gorecover(argp uintptr) interface{} {
gp := getg()
p := gp._panic
// 需要根据 argp的地址,判断是否在defer函数中被调用
if p != nil && !p.recovered && argp == uintptr(p.argp) {
// 设置标志位,上面gopanic中会对这个标志位做判断
p.recovered = true
return p.arg
}
return nil
}
goexit
当手动调用 runtime.Goexit()
退出的时候,defer函数也会执行:
func Goexit() {
// Run all deferred functions for the current goroutine.
// This code is similar to gopanic, see that implementation
// for detailed comments.
gp := getg()
// 遍历defer链表
for {
d := gp._defer
if d == nil {
break
}
// 如果 defer已经执行过了,与defer绑定的panic 终止掉
if d.started {
if d._panic != nil {
d._panic.aborted = true
d._panic = nil
}
d.fn = nil
// 从defer链表中移除
gp._defer = d.link
// 释放defer
freedefer(d)
continue
}
// 调用defer内部函数
d.started = true
reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))
if gp._defer != d {
throw("bad defer entry in Goexit")
}
d._panic = nil
d.fn = nil
gp._defer = d.link
freedefer(d)
// Note: we ignore recovers here because Goexit isn't a panic
}
// 调用goexit0,清除当前g的属性,重新进入调度
goexit1()
}
go中的关键字-defer的更多相关文章
- swift学习笔记 - swift中常用关键字
swift中常用关键字 **用作声明的关键字: ** class.deinit.enum.extension.func.import.init.let.protocol.static.struct.s ...
- Java中的关键字 transient
先解释下Java中的对象序列化 在讨论transient之前,有必要先搞清楚Java中序列化的含义: Java中对象的序列化指的是将对象转换成以字节序列的形式来表示,这些字节序列包含了对象的数据和信息 ...
- js中this关键字测试集锦
参考:阮一峰<javascript的this用法>及<JS中this关键字详解> this是Javascript语言的一个关键字它代表函数运行时,自动生成的一个内部对象,只能在 ...
- 【转载】C/C++中extern关键字详解
1 基本解释:extern可以置于变量或者函数前,以标示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义.此外extern也可用来进行链接指定. 也就是说extern ...
- 【转】java中volatile关键字的含义
java中volatile关键字的含义 在java线程并发处理中,有一个关键字volatile的使用目前存在很大的混淆,以为使用这个关键字,在进行多线程并发处理的时候就可以万事大吉. Java语言 ...
- 深入解析Javascript中this关键字的使用
深入解析Javascript中面向对象编程中的this关键字 在Javascript中this关键字代表函数运行时,自动生成的一个内部对象,只能在函数内部使用.比如: function TestFun ...
- C/C++中extern关键字解析
1 基本解释:extern可以置于变量或者函数前,以标示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义.此外extern也可用来进行链接指定. 也就是说extern ...
- C++中typename关键字的用法
我在我的 薛途的博客 上发表了新的文章,欢迎各位批评指正. C++中typename关键字的用法
- 【有人@我】Android中高亮变色显示文本中的关键字
应该是好久没有写有关技术类的文章了,前天还有人在群里问我,说群主很长时间没有分享干货了,今天分享一篇Android中TextView在大段的文字内容中如何让关键字高亮变色的文章 ,希望对大家有所帮助, ...
随机推荐
- vue 页面滚动到原位置
哈哈哈,昨天登QQ的时候,意外发现有人看了我写的博客,居然还加了我,这就激起了我内心的小波澜啊 公司最近在做电商,用的前端框架依然是VUE 矩MAX(微信公众号)可以搜的到哦,安卓商店或苹果AppSt ...
- 阿里云 centos7.X mysql数据库安装、配置
前言 1024阿里云搞活动,ecs云服务新用户促销活动,活动连接就不贴了,买了之后暂时不知道要做啥,想着先安装mysql数据库. 步骤 root用户登录进来 1.配置mysql的yum源,yum lo ...
- fullpage.js的引入方法
1.先到官网上(https://github.com/alvarotrigo/fullPage.js)下载压缩包 2.引入文件 3.布局基本页面结构 4.实现全屏滚动(JS代码) <script ...
- 玩转u8g2 OLED库 MAX7219_32X8点阵模块
u8g2 OLED库 + MAX7219_32X8点阵模块 理论基础 玩转u8g2 OLED库,一篇就够 玩转u8g2 OLED库,一篇就够(字数太多 要分篇) 实验内容 Full screen bu ...
- 深度讲解Linux内存管理和Linux进程调度-打通任督二脉
我在多年的工程生涯中发现很多工程师碰到一个共性的问题:Linux工程师很多,甚至有很多有多年工作经验,但是对一些关键概念的理解非常模糊,比如不理解CPU.内存资源等的真正分布,具体的工作机制,这使得他 ...
- C# WPF基础巩固
时间如流水,只能流去不流回. 学历代表你的过去,能力代表你的现在,学习能力代表你的将来. 学无止境,精益求精. 一.写作目的 做C# WPF开发,无论是工作中即将使用,还是只应付跳槽面试,开发基础是非 ...
- ThreadPoolExecutor源码中的适配器模式
什么是适配器模式 网上已有很多的教程,不细讲了.可以参考:五分钟了解设计模式(3)---适配器模式 在适配器模式中,一定要识别清楚,Target Adaptee Adapter分别是哪些类或接口,这样 ...
- FastJson的使用心得
本文为早前整理,参考文献已找不到,如有侵权请与我联系,添加参考链接. 一丶基本使用 1.1主要API fastjson入口类是com.alibaba.fastjson.JSON,主要的API是JS ...
- java中的时区转换
目录 java中的时区转换 一.时区的说明 二.时间的表示 三.时间戳 四.Date类和时间戳 五.java中的时区转换 java中的时区转换 一.时区的说明 地球表面按经线从东到西,被划成一个个区域 ...
- class定义类 及 实现继承
class 定义类 代码如下: class Student { constructor(name) { this.name = name; } sayHello() { console.log(&qu ...