文档章节

Go语言的函数调用信息

chai2010
 chai2010
发布于 2014/01/04 10:14
字数 3414
阅读 4932
收藏 78
点赞 12
评论 2

By chaishushan{AT}gmail.com

注: 本文初稿发在 Golang 中国博客, 这里的内容有部分修改.

函数的调用信息是程序中比较重要运行期信息, 在很多场合都会用到(比如调试或日志).

Go语言 runtime 包的 runtime.Caller / runtime.Callers / runtime.FuncForPC 等几个函数提供了获取函数调用者信息的方法.

这几个函数的文档链接:

本文主要讲述这几个函数的用法.

runtime.Caller 的用法

函数的签名如下:

func runtime.Caller(skip int) (pc uintptr, file string, line int, ok bool)

runtime.Caller 返回当前 goroutine 的栈上的函数调用信息. 主要有当前的 pc 值和调用的文件和行号等信息. 若无法获得信息, 返回的 ok 值为 false.

其输入参数 skip 为要跳过的栈帧数, 若为 0 则表示 runtime.Caller 的调用者.

注意:由于历史原因, runtime.Callerruntime.Callers 中的 skip 含义并不相同, 后面会讲到.

下面是一个简单的例子, 打印函数调用的栈帧信息:

func main() {
	for skip := 0; ; skip++ {
		pc, file, line, ok := runtime.Caller(skip)
		if !ok {
			break
		}
		fmt.Printf("skip = %v, pc = %v, file = %v, line = %v\n", skip, pc, file, line)
	}
	// Output:
	// skip = 0, pc = 4198453, file = caller.go, line = 10
	// skip = 1, pc = 4280066, file = $(GOROOT)/src/pkg/runtime/proc.c, line = 220
	// skip = 2, pc = 4289712, file = $(GOROOT)/src/pkg/runtime/proc.c, line = 1394
}

其中 skip = 0 为当前文件("caller.go")的 main.main 函数, 以及对应的行号. 这里省略的无关代码, 因此输出的行号和网页展示的位置有些差异.

另外的 skip = 1skip = 2 也分别对应2个函数调用. 通过查阅 runtime/proc.c 文件的代码, 我们可以知道对应的函数分别为 runtime.mainruntime.goexit.

整理之后可以知道, Go的普通程序的启动顺序如下:

  1. runtime.goexit 为真正的函数入口(并不是main.main)
  2. 然后 runtime.goexit 调用 runtime.main 函数
  3. 最终 runtime.main 调用用户编写的 main.main 函数

runtime.Callers 的用法

函数的签名如下:

func runtime.Callers(skip int, pc []uintptr) int

runtime.Callers 函数和 runtime.Caller 函数虽然名字相似(多一个后缀s), 但是函数的参数/返回值和参数的意义都有很大的差异.

runtime.Callers 把调用它的函数Go程栈上的程序计数器填入切片 pc 中. 参数 skip 为开始在 pc 中记录之前所要跳过的栈帧数, 若为0则表示 runtime.Callers 自身的栈帧, 若为1则表示调用者的栈帧. 该函数返回写入到 pc 切片中的项数(受切片的容量限制).

下面是 runtime.Callers 的例子, 用于输出每个栈帧的 pc 信息:

func main() {
	pc := make([]uintptr, 1024)
	for skip := 0; ; skip++ {
		n := runtime.Callers(skip, pc)
		if n <= 0 {
			break
		}
		fmt.Printf("skip = %v, pc = %v\n", skip, pc[:n])
	}
	// Output:
	// skip = 0, pc = [4304486 4198562 4280114 4289760]
	// skip = 1, pc = [4198562 4280114 4289760]
	// skip = 2, pc = [4280114 4289760]
	// skip = 3, pc = [4289760]
}

输出新的 pc 长度和 skip 大小有逆相关性. skip = 0runtime.Callers 自身的信息.

这个例子比前一个例子多输出了一个栈帧, 就是因为多了一个runtime.Callers栈帧的信息(前一个例子是没有runtime.Caller信息的(注意:没有s后缀)).

那么 runtime.Callersruntime.Caller 有哪些关联和差异?

runtime.Callersruntime.Caller 的异同

因为前面2个例子为不同的程序, 输出的 pc 值并不具备参考性. 现在我们看看在同一个例子的输出结果如何:

func main() {
	for skip := 0; ; skip++ {
		pc, file, line, ok := runtime.Caller(skip)
		if !ok {
			break
		}
		fmt.Printf("skip = %v, pc = %v, file = %v, line = %v\n", skip, pc, file, line)
	}
	// Output:
	// skip = 0, pc = 4198456, file = caller.go, line = 10
	// skip = 1, pc = 4280962, file = $(GOROOT)/src/pkg/runtime/proc.c, line = 220
	// skip = 2, pc = 4290608, file = $(GOROOT)/src/pkg/runtime/proc.c, line = 1394
	pc := make([]uintptr, 1024)
	for skip := 0; ; skip++ {
		n := runtime.Callers(skip, pc)
		if n <= 0 {
			break
		}
		fmt.Printf("skip = %v, pc = %v\n", skip, pc[:n])
	}
	// Output:
	// skip = 0, pc = [4305334 4198635 4280962 4290608]
	// skip = 1, pc = [4198635 4280962 4290608]
	// skip = 2, pc = [4280962 4290608]
	// skip = 3, pc = [4290608]
}

比如输出结果可以发现, 42809624290608 两个 pc 值是相同的. 它们分别对应 runtime.mainruntime.goexit 函数.

runtime.Caller 输出的 4198456runtime.Callers 输出的 4198635 并不相同. 这是因为, 这两个函数的调用位置并不相同, 因此导致了 pc 值也不完全相同.

最后就是 runtime.Callers 多输出一个 4305334 值, 对应runtime.Callers内部的调用位置.

由于Go语言(Go1.2)采用分段堆栈, 因此不同的 pc 之间的大小关系并不明显.

runtime.FuncForPC 的用途

函数的签名如下:

func runtime.FuncForPC(pc uintptr) *runtime.Func
func (f *runtime.Func) FileLine(pc uintptr) (file string, line int)
func (f *runtime.Func) Entry() uintptr
func (f *runtime.Func) Name() string

其中 runtime.FuncForPC 返回包含给定 pc 地址的函数, 如果是无效 pc 则返回 nil .

runtime.Func.FileLine 返回与 pc 对应的源码文件名和行号. 安装文档的说明, 如果pc不在函数帧范围内, 则结果是不确定的.

runtime.Func.Entry 对应函数的地址. runtime.Func.Name 返回该函数的名称.

下面是 runtime.FuncForPC 的例子:

func main() {
	for skip := 0; ; skip++ {
		pc, _, _, ok := runtime.Caller(skip)
		if !ok {
			break
		}
		p := runtime.FuncForPC(pc)
		file, line := p.FileLine(0)

		fmt.Printf("skip = %v, pc = %v\n", skip, pc)
		fmt.Printf("  file = %v, line = %d\n", file, line)
		fmt.Printf("  entry = %v\n", p.Entry())
		fmt.Printf("  name = %v\n", p.Name())
	}
	// Output:
	// skip = 0, pc = 4198456
	//   file = caller.go, line = 8
	//   entry = 4198400
	//   name = main.main
	// skip = 1, pc = 4282882
	//   file = $(GOROOT)/src/pkg/runtime/proc.c, line = 179
	//   entry = 4282576
	//   name = runtime.main
	// skip = 2, pc = 4292528
	//   file = $(GOROOT)/src/pkg/runtime/proc.c, line = 1394
	//   entry = 4292528
	//   name = runtime.goexit
	pc := make([]uintptr, 1024)
	for skip := 0; ; skip++ {
		n := runtime.Callers(skip, pc)
		if n <= 0 {
			break
		}
		fmt.Printf("skip = %v, pc = %v\n", skip, pc[:n])
		for j := 0; j < n; j++ {
			p := runtime.FuncForPC(pc[j])
			file, line := p.FileLine(0)

			fmt.Printf("  skip = %v, pc = %v\n", skip, pc[j])
			fmt.Printf("    file = %v, line = %d\n", file, line)
			fmt.Printf("    entry = %v\n", p.Entry())
			fmt.Printf("    name = %v\n", p.Name())
		}
		break
	}
	// Output:
	// skip = 0, pc = [4307254 4198586 4282882 4292528]
	//   skip = 0, pc = 4307254
	//     file = $(GOROOT)/src/pkg/runtime/runtime.c, line = 315
	//     entry = 4307168
	//     name = runtime.Callers
	//   skip = 0, pc = 4198586
	//     file = caller.go, line = 8
	//     entry = 4198400
	//     name = main.main
	//   skip = 0, pc = 4282882
	//     file = $(GOROOT)/src/pkg/runtime/proc.c, line = 179
	//     entry = 4282576
	//     name = runtime.main
	//   skip = 0, pc = 4292528
	//     file = $(GOROOT)/src/pkg/runtime/proc.c, line = 1394
	//     entry = 4292528
	//     name = runtime.goexit
}

根据测试, 如果是无效 pc (比如0), runtime.Func.FileLine 一般会输出当前函数的开始行号. 不过在实践中, 一般会用 runtime.Caller 获取文件名和行号信息, runtime.Func.FileLine 很少用到(如何独立获取pc参数?).

定制的 CallerName 函数

基于前面的几个函数, 我们可以方便的定制一个 CallerName 函数. 函数 CallerName 返回调用者的函数名/文件名/行号等用户友好的信息.

函数实现如下:

func CallerName(skip int) (name, file string, line int, ok bool) {
	var pc uintptr
	if pc, file, line, ok = runtime.Caller(skip + 1); !ok {
		return
	}
	name = runtime.FuncForPC(pc).Name()
	return
}

其中在执行 runtime.Caller 调用时, 参数 skip + 1 用于抵消 CallerName 函数自身的调用.

下面是基于 CallerName 的输出例子:

func main() {
	for skip := 0; ; skip++ {
		name, file, line, ok := CallerName(skip)
		if !ok {
			break
		}
		fmt.Printf("skip = %v\n", skip)
		fmt.Printf("  file = %v, line = %d\n", file, line)
		fmt.Printf("  name = %v\n", name)
	}
	// Output:
	// skip = 0
	//   file = caller.go, line = 19
	//   name = main.main
	// skip = 1
	//   file = $(GOROOT)/src/pkg/runtime/proc.c, line = 220
	//   name = runtime.main
	// skip = 2
	//   file = $(GOROOT)/src/pkg/runtime/proc.c, line = 1394
	//   name = runtime.goexit
}

这样就可以方便的输出函数调用者的信息了.

Go语言中函数的类型

在Go语言中, 除了语言定义的普通函数调用外, 还有闭包函数/init函数/全局变量初始化等不同的函数调用类型.

为了便于测试不同类型的函数调用, 我们包装一个 PrintCallerName 函数. 该函数用于输出调用者的信息.

func PrintCallerName(skip int, comment string) bool {
	name, file, line, ok := CallerName(skip + 1)
	if !ok {
		return false
	}
	fmt.Printf("skip = %v, comment = %s\n", skip, comment)
	fmt.Printf("  file = %v, line = %d\n", file, line)
	fmt.Printf("  name = %v\n", name)
	return true
}

然后编写以下的测试代码(函数闭包调用/全局变量初始化/init函数等):

var a = PrintCallerName(0, "main.a")
var b = PrintCallerName(0, "main.b")

func init() {
	a = PrintCallerName(0, "main.init.a")
}

func init() {
	b = PrintCallerName(0, "main.init.b")
	func() {
		b = PrintCallerName(0, "main.init.b[1]")
	}()
}

func main() {
	a = PrintCallerName(0, "main.main.a")
	b = PrintCallerName(0, "main.main.b")
	func() {
		b = PrintCallerName(0, "main.main.b[1]")
		func() {
			b = PrintCallerName(0, "main.main.b[1][1]")
		}()
		b = PrintCallerName(0, "main.main.b[2]")
	}()
}

输出结果如下:

// Output:
// skip = 0, comment = main.a
//   file = caller.go, line = 8
//   name = main.init
// skip = 0, comment = main.b
//   file = caller.go, line = 9
//   name = main.init
// skip = 0, comment = main.init.a
//   file = caller.go, line = 12
//   name = main.init·1
// skip = 0, comment = main.init.b
//   file = caller.go, line = 16
//   name = main.init·2
// skip = 0, comment = main.init.b[1]
//   file = caller.go, line = 18
//   name = main.func·001
// skip = 0, comment = main.main.a
//   file = caller.go, line = 23
//   name = main.main
// skip = 0, comment = main.main.b
//   file = caller.go, line = 24
//   name = main.main
// skip = 0, comment = main.main.b[1]
//   file = caller.go, line = 26
//   name = main.func·003
// skip = 0, comment = main.main.b[1][1]
//   file = caller.go, line = 28
//   name = main.func·002
// skip = 0, comment = main.main.b[2]
//   file = caller.go, line = 30
//   name = main.func·003

观察输出结果, 可以发现以下几个规律:

  • 全局变量的初始化调用者为 main.init 函数
  • 自定义的 init 函数有一个数字后缀, 根据出现的顺序进编号. 比如 main.init·1main.init·2 等.
  • 闭包函数采用 main.func·001 格式命名, 安装闭包定义结束的位置顺序进编号.

比如以下全局变量的初始化调用者为 main.init 函数:

var a = PrintCallerName(0, "main.a")
var b = PrintCallerName(0, "main.b")

以下两个 init 函数根据出现顺序分别对应 main.init·1main.init·2 :

func init() { // main.init·1
	//
}
func init() { // main.init·2
	//
}

以下三个闭包根据定义结束顺序分别为 001 / 002 / 003 :

func init() {
	func(){
		//
	}() // main.func·001
}

func main() {
	func() {
		func(){
			//
		}() // main.func·002
	}() // main.func·003
}

因为, 这些特殊函数调用方式的存在, 我们需要进一步完善 CallerName 函数.

改进的 CallerName 函数

两类特殊的调用是 init 类函数调用 和 闭包函数调用.

改进后的 CallerName 函数对 init 类函数调用者统一处理为 init 函数. 将闭包函数调用这处理为调用者的函数名.

// caller types:
// runtime.goexit
// runtime.main
// main.init
// main.init·1
// main.main
// main.func·001
// code.google.com/p/gettext-go/gettext.TestCallerName
// ...
func CallerName(skip int) (name, file string, line int, ok bool) {
	var (
		reInit    = regexp.MustCompile(`init·\d+$`) // main.init·1
		reClosure = regexp.MustCompile(`func·\d+$`) // main.func·001
	)
	for {
		var pc uintptr
		if pc, file, line, ok = runtime.Caller(skip + 1); !ok {
			return
		}
		name = runtime.FuncForPC(pc).Name()
		if reInit.MatchString(name) {
			name = reInit.ReplaceAllString(name, "init")
			return
		}
		if reClosure.MatchString(name) {
			skip++
			continue
		}
		return
	}
	return
}

处理的思路:

  1. 如果是 init 类型的函数调用(匹配正则表达式"init·\d+$"), 直接作为 init 函数范返回
  2. 如果是 func 闭包类型(匹配正则表达式"func·\d+$"), 跳过当前栈帧, 继续递归处理
  3. 返回普通的函数调用类型

CallerName 函数的不足之处

有以下的代码:

func init() {
	var _ = myInit("1")
}
func main() {
	var _ = myInit("2")
}

var myInit = func(name string) {
	b = PrintCallerName(0, name + ":main.myInit.b")
}

myInit 为一个全局变量, 被赋值为一个闭包函数. 然后在 initmain 函数分别调用 myInit 这个闭包函数输出的结果 会因为调用环境的不同而有差异.

从直观上看, myInit闭包函数在执行时, 最好输出 main.myInit 函数名. 但是 main.myInit 只是一个绑定到闭包函数的变量, 而闭包的真正名字是 main.func·???. 在运行时是无法得到 main.myInit 这个名字的.

因此在 gettext-go 中内部用的 callerName 函数采用将 main.func·??? 统一处理为 main.func 的, 然后作为 gettext.Gettext 翻译函数的上下文.

gettext-gocallerName 函数实现在这里: caller.go. 测试文件在这里: caller_test.go.

不同Go程序启动流程

基于函数调用者信息可以很容易的验证各种环境的程序启动流程.

我们需要建立一个独立的 caller 目录, 里面有三个测试代码.

caller/main.go 主程序:

package main

import (
	"fmt"
	"regexp"
	"runtime"
)

func main() {
	_ = PrintCallerName(0, "main.main._")
}

func PrintCallerName(skip int, comment string) bool {
	// 实现和前面的例子相同
}

func CallerName(skip int) (name, file string, line int, ok bool) {
	// 实现和前面的例子相同
}

caller/main_test.go 主程序的测试文件(同在一个main包):

package main

import (
	"fmt"
	"testing"
)

func TestPrintCallerName(t *testing.T) {
	for skip := 0; ; skip++ {
		name, file, line, ok := CallerName(skip)
		if !ok {
			break
		}
		fmt.Printf("skip = %v, name = %v, file = %v, line = %v\n", skip, name, file, line)
	}
	t.Fail()
}

caller/example_test.go 主程序的包的调用者(在新的main_test包):

package main_test

import (
	myMain "."
	"fmt"
)

func Example() {
	for skip := 0; ; skip++ {
		name, file, line, ok := myMain.CallerName(skip)
		if !ok {
			break
		}
		fmt.Printf("skip = %v, name = %v, file = %v, line = %v\n", skip, name, file, line)
	}
	// Output: ?
}

然后进入 caller 目录, 运行 go run test 可以得到以下的输出结果:

skip = 0, name = caller.TestPrintCallerName, file = caller/main_test.go, line = 10
skip = 1, name = testing.tRunner, file = $(GOROOT)/src/pkg/testing/testing.go, line = 391
skip = 2, name = runtime.goexit, file = $(GOROOT)/src/pkg/runtime/proc.c, line = 1394
--- FAIL: TestPrintCallerName (0.00 seconds)
--- FAIL: Example (2.0001ms)
got:
skip = 0, name = caller_test.Example, file = caller/example_test.go, line = 10

skip = 1, name = testing.runExample, file = $(GOROOT)/src/pkg/testing/example.go, line = 98
skip = 2, name = testing.RunExamples, file = $(GOROOT)/src/pkg/testing/example.go, line = 36
skip = 3, name = testing.Main, file = $(GOROOT)/src/pkg/testing/testing.go, line = 404
skip = 4, name = main.main, file = $(TEMP)/go-build365033523/caller/_test/_testmain.go, line = 51
skip = 5, name = runtime.main, file = $(GOROOT)/src/pkg/runtime/proc.c, line = 220
skip = 6, name = runtime.goexit, file = $(GOROOT)/src/pkg/runtime/proc.c, line = 1394
want:
?
FAIL
exit status 1
FAIL    caller        0.254s

分析输出数据我们可以发现, 测试代码和例子代码的启动流程和普通的程序流程都不太一样.

测试代码的启动流程:

  1. runtime.goexit 还是入口
  2. 但是 runtime.goexit 不在调用 runtime.main 函数, 而是调用 testing.tRunner 函数
  3. testing.tRunner 函数由 go test 命令生成, 用于执行各个测试函数

例子代码的启动流程:

  1. runtime.goexit 还是入口
  2. 然后 runtime.goexit 调用 runtime.main 函数
  3. 最终 runtime.main 调用go test 命令生成的 main.main 函数, 在 _test/_testmain.go 文件
  4. 然后调用 testing.Main, 改函数执行各个例子函数

另外, 从这个例子我们可以发现, 我们自己写的 main.main 函数所在的 main 包也可以被其他包导入. 但是其他包导入之后的 main 包里的 main 函数就不再是main.main 函数了. 因此, 程序的入口也就不是自己写的 main.main 函数了.

2015.06.09补充: 更深入的可以看下这个文章 GO语解惑:从源码分析GO程序的入口

总结

Go语言 runtime 包的 runtime.Caller / runtime.Callers / runtime.FuncForPC 等函数虽然看起来比较简单, 但是功能却非常强大.

这几个函数不仅可以解决一些实际的工程问题(比如 gettext-go 中用于获取翻译的上下文信息), 而且非常适合用于调试和分析各种Go程序的运行时信息.


https://chai2010.cn/

© 著作权归作者所有

共有 人打赏支持
chai2010

chai2010

粉丝 413
博文 98
码字总数 81391
作品 8
武汉
程序员
加载中

评论(2)

郑柯
郑柯
恩,开始认真学习
五杀联盟
五杀联盟
很棒!golang最近势头不小!
函数是第一类值和第二类值与回调函数

有些语言确实是不区分的,它的function(表示code)跟int, double的地位是一样的。这种语言就为函数是第一类值。 但问题是,有些语言是不能存储函数,不能动态创建函数,不能动态销毁函数。(这...

buleberry ⋅ 2014/03/13 ⋅ 0

Go基础编程:异常处理(error接口、panic、recover)

1 error接口 Go语言引入了一个关于错误处理的标准模式,即error接口,它是Go语言内建的接口类型,该接口的定义如下: Go语言的标准库代码包errors为用户提供如下方法: 另一个可以生成error...

tennysonsky ⋅ 01/01 ⋅ 0

Go 语言系统调用简析

Go 语言系统调用简析 Go语言学习园地博客2016-06-08114 阅读 一、系统调用概述 系统调用是受控的内核入口,借助于这一机制,进程可以请求内核以自己的名义去执行某些动作。Linux 内核以 C 语...

Go语言学习园地博客 ⋅ 2016/06/08 ⋅ 0

游戏中使用LUA脚本语言的简介

我们知道脚本语言是解除硬编码,防止重编译的利器,可以这样说,任何大型游戏都有自己的脚本系统。 想要做出一款精品游戏,脚本语言也是我们需要掌握和运用的。 较流行的脚本语言有Python,LUA,...

长平狐 ⋅ 2012/11/12 ⋅ 0

Golang 异常处理机制——defer, error, panic, recover

一、前言 在实际的项目中,对于异常的最佳实践很多,在使用不同的语言开发不同类型的程序时,有不同的建议。Google C++ Style 中提到 Google 内部的 C++ 代码中不使用异常,社区也有很多关于...

吃一堑消化不良 ⋅ 2016/12/01 ⋅ 0

OC语言的特性(一)-消息传递与调用函数的表现形式

我们在初学Objective-C时,都会觉得ObjC中的消息传递和其他语言的调用函数差不多,只是在OC中,方法调用用消息传递这一概念来代替。 那么到底怎样区别OC中的消息传递与其他语言的调用函数呢。...

浩浩老师 ⋅ 2015/09/08 ⋅ 0

尾调用优化

尾调用(Tail Call)是函数式编程的一个重要概念,本文介绍它的含义和用法。 一、什么是尾调用? 尾调用的概念非常简单,一句话就能说清楚,就是指某个函数的最后一步是调用另一个函数。 上面...

阮一峰 ⋅ 2015/04/10 ⋅ 0

Thrift之代码生成器Compiler原理及源码详细解析2

我的新浪微博:http://weibo.com/freshairbrucewoo。 欢迎大家相互交流,共同提高技术。 2 tgenerator类和tgenerator_registry类 这个两个类的主要功能就是为生成所有语言的代码提供基础信息...

xumaojun ⋅ 04/21 ⋅ 0

Thrift之代码生成器Compiler原理及源码详细解析1

我的新浪微博:http://weibo.com/freshairbrucewoo。 欢迎大家相互交流,共同提高技术。 又很久没有写博客了,最近忙着研究GlusterFS,本来周末打算写几篇博客的,但是由于调试GlusterFS的一...

xumaojun ⋅ 04/21 ⋅ 0

Go 语言的错误处理机制引发争议

最近,有关Go语言的错误处理机制在社区中展开了讨论,有人认为冗长重复的错误处理格式像是回到了上世纪七十年代,而Go语言的开发者给予了反驳。 Go语言的错误处理机制可以从支持函数多返回值...

红薯 ⋅ 2012/12/04 ⋅ 32

没有更多内容

加载失败,请刷新页面

加载更多

下一页

zblog2.3版本的asp系统是否可以超越卢松松博客的流量[图]

最近访问zblog官网,发现zlbog-asp2.3版本已经进入测试阶段了,虽然正式版还没有发布,想必也不久了。那么作为aps纵横江湖十多年的今天,blog2.2版本应该已经成熟了,为什么还要发布这个2.3...

原创小博客 ⋅ 53分钟前 ⋅ 0

聊聊spring cloud的HystrixCircuitBreakerConfiguration

序 本文主要研究一下spring cloud的HystrixCircuitBreakerConfiguration HystrixCircuitBreakerConfiguration spring-cloud-netflix-core-2.0.0.RELEASE-sources.jar!/org/springframework/......

go4it ⋅ 今天 ⋅ 0

二分查找

二分查找,也称折半查找、二分搜索,是一种在有序数组中查找某一特定元素的搜索算法。搜素过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜素过程结束;如果某一特定元素大于...

人觉非常君 ⋅ 今天 ⋅ 0

VS中使用X64汇编

需要注意的是,在X86项目中,可以使用__asm{}来嵌入汇编代码,但是在X64项目中,再也不能使用__asm{}来编写嵌入式汇编程序了,必须使用专门的.asm汇编文件来编写相应的汇编代码,然后在其它地...

simpower ⋅ 今天 ⋅ 0

ThreadPoolExecutor

ThreadPoolExecutor public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, ......

4rnold ⋅ 昨天 ⋅ 0

Java正无穷大、负无穷大以及NaN

问题来源:用Java代码写了一个计算公式,包含除法和对数和取反,在页面上出现了-infinity,不知道这是什么问题,网上找答案才明白意思是负的无穷大。 思考:为什么会出现这种情况呢?这是哪里...

young_chen ⋅ 昨天 ⋅ 0

前台对中文编码,后台解码

前台:encodeURI(sbzt) 后台:String param = URLDecoder.decode(sbzt,"UTF-8");

west_coast ⋅ 昨天 ⋅ 0

实验楼—MySQL基础课程-挑战3实验报告

按照文档要求创建数据库 sudo sercice mysql startwget http://labfile.oss.aliyuncs.com/courses/9/createdb2.sqlvim /home/shiyanlou/createdb2.sql#查看下数据库代码 代码创建了grade......

zhangjin7 ⋅ 昨天 ⋅ 0

一起读书《深入浅出nodejs》-node模块机制

node 模块机制 前言 说到node,就不免得提到JavaScript。JavaScript自诞生以来,经历了工具类库、组件库、前端框架、前端应用的变迁。通过无数开发人员的努力,JavaScript不断被类聚和抽象,...

小草先森 ⋅ 昨天 ⋅ 0

Java桌球小游戏

其实算不上一个游戏,就是两张图片,不停的重画,改变ball图片的位置。一个左右直线碰撞的,一个有角度碰撞的。 左右直线碰撞 package com.bjsxt.test;import javax.swing.*;import j...

森林之下 ⋅ 昨天 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部