详解go中panic源码解读

panic源码解读

前言

本文是在go version go1.13.15 darwin/amd64上进行的

panic的作用

  • panic能够改变程序的控制流,调用panic后会立刻停止执行当前函数的剩余代码,并在当前Goroutine中递归执行调用方的defer
  • recover可以中止panic造成的程序崩溃。它是一个只能在defer中发挥作用的函数,在其他作用域中调用不会发挥作用;

举个栗子

package main

import "fmt"

func main() {
	fmt.Println(1)
	func() {
		fmt.Println(2)
		panic("3")
	}()
	fmt.Println(4)
}

输出

1
2
panic: 3

goroutine 1 [running]:
main.main.func1(...)
        /Users/yj/Go/src/Go-POINT/panic/main.go:9
main.main()
        /Users/yj/Go/src/Go-POINT/panic/main.go:10 +0xee

panic后会立刻停止执行当前函数的剩余代码,所以4没有打印出来

对于recover

  • panic只会触发当前Goroutine的defer;
  • recover只有在defer中调用才会生效;
  • panic允许在defer中嵌套多次调用;
package main

import (
	"fmt"
	"time"
)

func main() {
	fmt.Println(1)

	defer func() {
		if err := recover(); err != nil {
			fmt.Println(err)
		}
	}()

	go func() {
		fmt.Println(2)
		panic("3")
	}()
	time.Sleep(time.Second)
	fmt.Println(4)
}

上面的栗子,因为recoverpanic不在同一个goroutine中,所以不会捕获到

嵌套的demo

func main() {
	defer fmt.Println("in main")
	defer func() {
		defer func() {
			panic("3 panic again and again")
		}()
		panic("2 panic again")
	}()

	panic("1 panic once")
}

输出

in main
panic: 1 panic once
        panic: 2 panic again
        panic: 3 panic again and again

goroutine 1 [running]:
...

多次调用panic也不会影响defer函数的正常执行,所以使用defer进行收尾工作一般来说都是安全的。

panic使用场景

  • error:可预见的错误
  • panic:不可预见的异常

需要注意的是,你应该尽可能地使用error,而不是使用panicrecover。只有当程序不能继续运行的时候,才应该使用panicrecover机制。

panic有两个合理的用例。

1、发生了一个不能恢复的错误,此时程序不能继续运行。 一个例子就是 web 服务器无法绑定所要求的端口。在这种情况下,就应该使用 panic,因为如果不能绑定端口,啥也做不了。

2、发生了一个编程上的错误。 假如我们有一个接收指针参数的方法,而其他人使用 nil 作为参数调用了它。在这种情况下,我们可以使用panic,因为这是一个编程错误:用 nil 参数调用了一个只能接收合法指针的方法。

在一般情况下,我们不应通过调用panic函数来报告普通的错误,而应该只把它作为报告致命错误的一种方式。当某些不应该发生的场景发生时,我们就应该调用panic。

总结下panic的使用场景:

1、空指针引用

2、下标越界

3、除数为0

4、不应该出现的分支,比如default

5、输入不应该引起函数错误

看下实现

先来看下_panic的结构

// _panic 保存了一个活跃的 panic
//
// 这个标记了 go:notinheap 因为 _panic 的值必须位于栈上
//
// argp 和 link 字段为栈指针,但在栈增长时不需要特殊处理:因为他们是指针类型且
// _panic 值只位于栈上,正常的栈指针调整会处理他们。
//
//go:notinheap
type _panic struct {
	argp      unsafe.Pointer // panic 期间 defer 调用参数的指针; 无法移动 - liblink 已知
	arg       interface{}    // panic的参数
	link      *_panic        // link 链接到更早的 panic
	recovered bool           // panic是否结束
	aborted   bool           // panic是否被忽略
}

link指向了保存在goroutine链表中先前的panic链表

gopanic

编译器会将panic装换成gopanic,来看下执行的流程:

1、创建新的runtime._panic并添加到所在Goroutine的_panic链表的最前面;

2、在循环中不断从当前Goroutine 的_defer中链表获取runtime._defer并调用runtime.reflectcall运行延迟调用函数;

3、调用runtime.fatalpanic中止整个程序;

// 预先声明的函数 panic 的实现
func gopanic(e interface{}) {
	gp := getg()
	// 判断在系统栈上还是在用户栈上
	// 如果执行在系统或信号栈时,getg() 会返回当前 m 的 g0 或 gsignal
	// 因此可以通过 gp.m.curg == gp 来判断所在栈
	// 系统栈上的 panic 无法恢复
	if gp.m.curg != gp {
		print("panic: ")
		printany(e)
		print("\n")
		throw("panic on system stack")
	}
	// 如果正在进行 malloc 时发生 panic 也无法恢复
	if gp.m.mallocing != 0 {
		print("panic: ")
		printany(e)
		print("\n")
		throw("panic during malloc")
	}
	// 在禁止抢占时发生 panic 也无法恢复
	if gp.m.preemptoff != "" {
		print("panic: ")
		printany(e)
		print("\n")
		print("preempt off reason: ")
		print(gp.m.preemptoff)
		print("\n")
		throw("panic during preemptoff")
	}
	// 在 g 锁在 m 上时发生 panic 也无法恢复
	if gp.m.locks != 0 {
		print("panic: ")
		printany(e)
		print("\n")
		throw("panic holding locks")
	}

	// 下面是可以恢复的
	var p _panic
	p.arg = e
	// panic 保存了对应的消息,并指向了保存在 goroutine 链表中先前的 panic 链表
	p.link = gp._panic
	gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))

	atomic.Xadd(&runningPanicDefers, 1)

	for {
		// 开始逐个取当前 goroutine 的 defer 调用
		d := gp._defer
		// 没有defer,退出循环
		if d == nil {
			break
		}

		// 如果 defer 是由早期的 panic 或 Goexit 开始的(并且,因为我们回到这里,这引发了新的 panic),
		// 则将 defer 带离链表。更早的 panic 或 Goexit 将无法继续运行。
		if d.started {
			if d._panic != nil {
				d._panic.aborted = true
			}
			d._panic = nil
			d.fn = nil
			gp._defer = d.link
			freedefer(d)
			continue
		}

		// 将deferred标记为started
		// 如果栈增长或者垃圾回收在 reflectcall 开始执行 d.fn 前发生
		// 标记 defer 已经开始执行,但仍将其保存在列表中,从而 traceback 可以找到并更新这个 defer 的参数帧

		// 标记defer是否已经执行
		d.started = true

		// 记录正在运行的延迟的panic。
		// 如果在延迟调用期间有新的panic,那么这个panic
		// 将在列表中找到d,并将标记d._panic(此panic)中止。
		d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))

		p.argp = unsafe.Pointer(getargp(0))

		reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))
		p.argp = nil

		// reflectcall没有panic。删除d
		if gp._defer != d {
			throw("bad defer entry in panic")
		}
		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)
		if p.recovered {
			atomic.Xadd(&runningPanicDefers, -1)

			gp._panic = p.link
			// 忽略的 panic 会被标记,但仍然保留在 g.panic 列表中
			// 这里将它们移出列表
			for gp._panic != nil && gp._panic.aborted {
				gp._panic = gp._panic.link
			}
			if gp._panic == nil { // 必须由 signal 完成
				gp.sig = 0
			}
			// 传递关于恢复帧的信息
			gp.sigcode0 = uintptr(sp)
			gp.sigcode1 = pc
			// 调用 recover,并重新进入调度循环,不再返回
			mcall(recovery)
			// 如果无法重新进入调度循环,则无法恢复错误
			throw("recovery failed") // mcall should not return
		}
	}

	// 消耗完所有的 defer 调用,保守地进行 panic
	// 因为在冻结之后调用任意用户代码是不安全的,所以我们调用 preprintpanics 来调用
	// 所有必要的 Error 和 String 方法来在 startpanic 之前准备 panic 字符串。
	preprintpanics(gp._panic)

	fatalpanic(gp._panic) // 不应该返回
	*(*int)(nil) = 0      // 无法触及
}

// reflectcall 使用 arg 指向的 n 个参数字节的副本调用 fn。
// fn 返回后,reflectcall 在返回之前将 n-retoffset 结果字节复制回 arg+retoffset。
// 如果重新复制结果字节,则调用者应将参数帧类型作为 argtype 传递,以便该调用可以在复制期间执行适当的写障碍。
// reflect 包传递帧类型。在 runtime 包中,只有一个调用将结果复制回来,即 cgocallbackg1,
// 并且它不传递帧类型,这意味着没有调用写障碍。参见该调用的页面了解相关理由。
//
// 包 reflect 通过 linkname 访问此符号
func reflectcall(argtype *_type, fn, arg unsafe.Pointer, argsize uint32, retoffset uint32)

梳理下流程

1、在处理panic期间,会先判断当前panic的类型,确定panic是否可恢复;

  • 系统栈上的panic无法恢复
  • 如果正在进行malloc时发生panic也无法恢复
  • 在禁止抢占时发生panic也无法恢复
  • 在g锁在m上时发生panic也无法恢复

2、可恢复的panicpaniclink指向goroutine链表中先前的panic链表;

3、循环逐个获取当前goroutinedefer调用;

  • 如果defer是由早期panic或Goexit开始的,则将defer带离链表,更早的panic或Goexit将无法继续运行,也就是将之前的panic终止掉,将aborted设置为true,在下面执行recover时保证goexit不会被取消;
  • recovered会在gorecover中被标记,见下文。当recovered被标记为true时,recovery函数触发Goroutine的调度,调度之前会准备好 sp、pc 以及函数的返回值;
  • 当延迟函数中recover了一个panic时,就会返回1,当runtime.deferproc函数的返回值是1时,编译器生成的代码会直接跳转到调用方函数返回之前并执行runtime.deferreturn,跳转到runtime.deferturn函数之后,程序就已经从panic恢复了正常的逻辑。而runtime.gorecover函数也能从runtime._panic结构中取出了调用panic时传入的arg参数并返回给调用方。
// 在发生 panic 后 defer 函数调用 recover 后展开栈。然后安排继续运行,
// 就像 defer 函数的调用方正常返回一样。
func recovery(gp *g) {
	// Info about defer passed in G struct.
	sp := gp.sigcode0
	pc := gp.sigcode1

	// d's arguments need to be in the stack.
	if sp != 0 && (sp < gp.stack.lo || gp.stack.hi < sp) {
		print("recover: ", hex(sp), " not in [", hex(gp.stack.lo), ", ", hex(gp.stack.hi), "]\n")
		throw("bad recovery")
	}

	// 使 deferproc 为此 d 返回
	// 这时候返回 1。调用函数将跳转到标准的返回尾声
	gp.sched.sp = sp
	gp.sched.pc = pc
	gp.sched.lr = 0
	gp.sched.ret = 1
	gogo(&gp.sched)
}

recovery函数中,利用g中的两个状态码回溯栈指针sp并恢复程序计数器pc到调度器中,并调用gogo重新调度g,将g恢复到调用recover函数的位置,goroutine继续执行,recovery在调度过程中会将函数的返回值设置为1。调用函数将跳转到标准的返回尾声。

func deferproc(siz int32, fn *funcval) { // arguments of fn follow fn
	...

	// deferproc returns 0 normally.
	// a deferred func that stops a panic
	// makes the deferproc return 1.
	// the code the compiler generates always
	// checks the return value and jumps to the
	// end of the function if deferproc returns != 0.
	return0()
	// No code can go here - the C return register has
	// been set and must not be clobbered.
}

当延迟函数中recover了一个panic时,就会返回1,当runtime.deferproc函数的返回值是1时,编译器生成的代码会直接跳转到调用方函数返回之前并执行runtime.deferreturn,跳转到runtime.deferturn函数之后,程序就已经从panic恢复了正常的逻辑。而runtime.gorecover函数也能从runtime._panic结构中取出了调用panic时传入的arg参数并返回给调用方。

gorecover

编译器会将recover装换成gorecover

如果recover被正确执行了,也就是gorecover,那么recovered将被标记成true

// go/src/runtime/panic.go
// 执行预先声明的函数 recover。
// 不允许分段栈,因为它需要可靠地找到其调用者的栈段。
//
// TODO(rsc): Once we commit to CopyStackAlways,
// this doesn't need to be nosplit.
//go:nosplit
func gorecover(argp uintptr) interface{} {
	// 必须在 panic 期间作为 defer 调用的一部分在函数中运行。
	// 必须从调用的最顶层函数( defer 语句中使用的函数)调用。
	// p.argp 是最顶层 defer 函数调用的参数指针。
	// 比较调用方报告的 argp,如果匹配,则调用者可以恢复。
	gp := getg()
	p := gp._panic
	if p != nil && !p.recovered && argp == uintptr(p.argp) {
		// 标记recovered
		p.recovered = true
		return p.arg
	}
	return nil
}

在正常情况下,它会修改runtime._panicrecovered字段,runtime.gorecover函数中并不包含恢复程序的逻辑,程序的恢复是由runtime.gopanic函数负责。

gorecoverrecovered标记为true,然后gopanic就可以通过mcall调用recovery并重新进入调度循环

fatalpanic

runtime.fatalpanic实现了无法被恢复的程序崩溃,它在中止程序之前会通过runtime.printpanics打印出全部的panic消息以及调用时传入的参数:

// go/src/runtime/panic.go
// fatalpanic 实现了不可恢复的 panic。类似于 fatalthrow,
// 如果 msgs != nil,则 fatalpanic 仍然能够打印 panic 的消息
// 并在 main 在退出时候减少 runningPanicDeferss
//
//go:nosplit
func fatalpanic(msgs *_panic) {
	// 返回程序计数寄存器指针
	pc := getcallerpc()
	// 返回堆栈指针
	sp := getcallersp()
	// 返回当前G
	gp := getg()
	var docrash bool
	// 切换到系统栈来避免栈增长,如果运行时状态较差则可能导致更糟糕的事情
	systemstack(func() {
		if startpanic_m() && msgs != nil {
			// 有 panic 消息和 startpanic_m 则可以尝试打印它们

			// startpanic_m 设置 panic 会从阻止 main 的退出,
			// 因此现在可以开始减少 runningPanicDefers 了
			atomic.Xadd(&runningPanicDefers, -1)

			printpanics(msgs)
		}

		docrash = dopanic_m(gp, pc, sp)
	})

	if docrash {
		// 通过在上述 systemstack 调用之外崩溃,调试器在生成回溯时不会混淆。
		// 函数崩溃标记为 nosplit 以避免堆栈增长。
		crash()
	}
	// 从系统推出
	systemstack(func() {
		exit(2)
	})

	*(*int)(nil) = 0 // not reached
}

// 打印出当前活动的panic
func printpanics(p *_panic) {
	if p.link != nil {
		printpanics(p.link)
		print("\t")
	}
	print("panic: ")
	printany(p.arg)
	if p.recovered {
		print(" [recovered]")
	}
	print("\n")
}

总结

引一段来自【panic 和recover】的总结

1、编译器会负责做转换关键字的工作;

1、将panicrecover分别转换成runtime.gopanicruntime.gorecover

2、将defer转换成runtime.deferproc函数;

3、在调用defer的函数末尾调用runtime.deferreturn函数;

2、在运行过程中遇到runtime.gopanic方法时,会从Goroutine的链表依次取出runtime._defer结构体并执行;

3、如果调用延迟执行函数时遇到了runtime.gorecover就会将_panic.recovered标记成true并返回panic的参数;

1、在这次调用结束之后,runtime.gopanic会从runtime._defer结构体中取出程序计数器pc和栈指针sp并调用runtime.recovery函数进行恢复程序;

2、runtime.recovery会根据传入的pcsp跳转回runtime.deferproc

3、编译器自动生成的代码会发现runtime.deferproc的返回值不为0,这时会跳回runtime.deferreturn并恢复到正常的执行流程;

4、如果没有遇到runtime.gorecover就会依次遍历所有的runtime._defer,并在最后调用runtime.fatalpanic中止程序、打印panic的参数并返回错误码2

参考

【panic 和 recover】https://draveness.me/golang/docs/part2-foundation/ch05-keyword/golang-panic-recover/
【恐慌与恢复内建函数】https://golang.design/under-the-hood/zh-cn/part1basic/ch03lang/panic/
【Go语言panic/recover的实现】https://zhuanlan.zhihu.com/p/72779197
【panic and recover】https://eddycjy.gitbook.io/golang/di-6-ke-chang-yong-guan-jian-zi/panic-and-recover
【翻了源码,我把 panic 与 recover 给彻底搞明白了】https://jishuin.proginn.com/p/763bfbd4ed8c

到此这篇关于详解go中panic源码解读的文章就介绍到这了,更多相关go panic源码内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • go语言的panic和recover函数用法实例

    Golang 有2个内置的函数 panic() 和 recover(),用以报告和捕获运行时发生的程序错误,与 error 不同,panic-recover 一般用在函数内部.一定要注意不要滥用 panic-recover,可能会导致性能问题,我一般只在未知输入和不可靠请求时使用. golang 的错误处理流程:当一个函数在执行过程中出现了异常或遇到 panic(),正常语句就会立即终止,然后执行 defer 语句,再报告异常信息,最后退出 goroutine.如果在 defer 中使用了 re

  • go语言异常panic和恢复recover用法实例

    本文实例讲述了go语言异常panic和恢复recover用法.分享给大家供大家参考.具体分析如下: go中可以抛出一个panic的异常,然后在defer中通过recover捕获这个异常,然后正常处理 在一个主进程,多个go程处理逻辑的结构中,这个很重要,如果不用recover捕获panic异常,会导致整个进程出错中断 复制代码 代码如下: package main import "fmt" func main() { defer func() {     //必须要先声明defer,否

  • Golang捕获panic堆栈信息的讲解

    golang当中panic的时候如果启动的goroutine比较多,刷的信息满屏都是,在终端工具上因为刷的信息太多,找不到前边的信息,因此很有必要程序自己捕获panic,并且将错误信息输出到文件当中,以便定位排查问题. Golang捕获panic堆栈信息 func PanicTrace(kb int) []byte { s := []byte("/src/runtime/panic.go") e := []byte("\ngoroutine ") line := [

  • GO语言异常处理机制panic和recover分析

    本文实例分析了GO语言异常处理机制panic和recover.分享给大家供大家参考.具体如下: Golang 有2个内置的函数 panic() 和 recover(),用以报告和捕获运行时发生的程序错误,与 error 不同,panic-recover 一般用在函数内部.一定要注意不要滥用 panic-recover,可能会导致性能问题,我一般只在未知输入和不可靠请求时使用. golang 的错误处理流程:当一个函数在执行过程中出现了异常或遇到 panic(),正常语句就会立即终止,然后执行 d

  • Go中recover与panic区别详解

    概念 panic 与 recover 是 Go 的两个内置函数,这两个内置函数用于处理 Go 运行时的错误. panic用于主动抛出错误, recover 用来捕获panic 抛出的错误. func main() { //捕获 异常 defer func() { if p := recover(); p != nil { fmt.Printf("panic recover! p: %v", p) //类型判断 str, ok := p.(string) if ok { err := e

  • go panic时如何让函数返回数据?

    现在有这样一个问题:某函数如果正常执行,返回0,如果panic,则返回1,怎么搞呢? package main import "fmt" func test() int { defer func() { if err := recover(); err != nil { return 1 } }() var p *int *p = 0 return 0 } func main() { fmt.Println("ret is", test()) for {} } 这样

  • 详解go中panic源码解读

    panic源码解读 前言 本文是在go version go1.13.15 darwin/amd64上进行的 panic的作用 panic能够改变程序的控制流,调用panic后会立刻停止执行当前函数的剩余代码,并在当前Goroutine中递归执行调用方的defer: recover可以中止panic造成的程序崩溃.它是一个只能在defer中发挥作用的函数,在其他作用域中调用不会发挥作用: 举个栗子 package main import "fmt" func main() { fmt.

  • Mybatis-plus使用TableNameHandler分表详解(附完整示例源码)

    为什么要分表 Mysql是当前互联网系统中使用非常广泛的关系数据库,具有ACID的特性. 但是mysql的单表性能会受到表中数据量的限制,主要原因是B+树索引过大导致查询时索引无法全部加载到内存.读取磁盘的次数变多,而磁盘的每次读取对性能都有很大的影响. 这时一个简单可行的方案就是分表(当然土豪也可以堆硬件),将一张数据量庞大的表的数据,拆分到多个表中,这同时也减少了B+树索引的大小,减少磁盘读取次数,提高性能. 两种基础分表逻辑 说完了为什么要分表,下面聊聊业务开发中常见的两种基础的分表逻辑.

  • 详解SpringBoot自动配置源码

    一.引导加载自动配置类 @SpringBootApplication注解相当于@SpringBootConfiguration.@EnableAutoConfiguration.@ComponentScan这三个注解的整合 @SpringBootConfiguration 这个注解也使用了@Configuration标注,代表当前是一个配置类 @ComponentScan 包扫描,指定扫描哪些注解 @EnableAutoConfiguration 这个注解也是一个合成注解 @AutoConfig

  • 详解Java中的字节码增强技术

    目录 1.字节码增强技术 2.常见技术 3.ASM 3.1 测试 Main 3.2 测试 CustomerClassVisitor 3.3 测试 Test 1.字节码增强技术 字节码增强技术就是一类对现有字节码进行修改或者动态生成全新字节码文件的技术. 参考地址 2.常见技术 技术分类 类型 静态增强 AspectJ 动态增强 ASM.Javassist.Cglib.Java Proxy 3.ASM <dependency> <groupId>org.ow2.asm</gro

  • 详解IDEA创建Tomcat8源码工程流程

    上一篇文章的产出,其实离不开网上各位大神们的辅助,正是通过他们的讲解,我才对Tomcat的结构有了更进一步的认识. 但在描述前后端交互的过程中,还有很多细节并没有描述到位,所以就有了研究Tomcat源码的想法. 而在配置Tomcat源码工程的过程中,摸摸爬爬两个多小时,总算是成功启动了. 故撰写此篇博文,授之以渔. 准备工作 1.apache-tomcat-8.5.32-src源码包,官网下载并解压即可: 2.apache-ant-1.10.5(用的最新版)下载并安装:Tomcat源码默认采用的

  • 详解Element 指令clickoutside源码分析

    clickoutside是Element-ui实现的一个自定义指令,顾名思义,该指令用来处理目标节点之外的点击事件,常用来处理下拉菜单等展开内容的关闭,在Element-ui的Select选择器.Dropdown下拉菜单.Popover 弹出框等组件中都用到了该指令,所以这个指令在实现一些自定义组件的时候非常有用. 要分析该源码,首先要了解一下Vue的自定义指令.自定义指令的定义方式如下: // 注册一个全局自定义指令 Vue.directive('directiveName', { bind:

  • 详解从Vue.js源码看异步更新DOM策略及nextTick

    写在前面 因为对Vue.js很感兴趣,而且平时工作的技术栈也是Vue.js,这几个月花了些时间研究学习了一下Vue.js源码,并做了总结与输出. 文章的原地址:https://github.com/answershuto/learnVue. 在学习过程中,为Vue加上了中文的注释https://github.com/answershuto/learnVue/tree/master/vue-src,希望可以对其他想学习Vue源码的小伙伴有所帮助. 可能会有理解存在偏差的地方,欢迎提issue指出,

  • 详解CentOS 7.0源码包搭建LNMP 实际环境搭建

    Centos7+Nginx1.11.7+MySQL5.7.16+PHP7.1.0+openssl-1.1.0c 一.linux 系统限制配置 1.关闭系统防火墙 systemctl stop firewalld.service 关闭防火墙 systemctl disable firewalld.service 禁用防火墙 2.关闭SElinux sed -i 's/SELINUX=.*/SELINUX=disabled/g' /etc/selinux/config setenforce 0 se

  • Android编程动态加载布局实例详解【附demo源码】

    本文实例讲述了Android编程动态加载布局的方法.分享给大家供大家参考,具体如下: 由于前段时间项目需要,需要在一个页面上加载根据不同的按钮加载不同的布局页面,当时想到用 tabhot .不过美工提供的界面图完全用不上tabhot ,所以想到了动态加载的方法来解决这一需求.在这里我整理了一下,写了一个 DEMO 希望大家以后少走点弯路. 首先,我们先把界面的框架图画出来,示意图如下: 中间白色部门是一个线性布局文件,我喜欢在画图的时候用不同的颜色将一块布局标示出来,方便查看.布局文件代码如下:

  • 微信小程序 授权登录详解(附完整源码)

    一.前言 由于微信官方修改了 getUserInfo 接口,所以现在无法实现一进入微信小程序就弹出授权窗口,只能通过 button 去触发. 官方连接:https://developers.weixin.qq.com/community/develop/doc/0000a26e1aca6012e896a517556c01 二.实现思路 自己写一个微信授权登录页面让用户实现点击的功能,也就是实现了通过 button 组件去触发 getUserInof 接口.在用户进入微信小程序的时候,判断用户是否

随机推荐