GO-生命周期

news2024/10/1 21:38:10

好奇心

出于好奇,想了解go的执行生命周期,于是尝试跟着go的源码看看执行过程

go源码地址:GitHub - golang/go: The Go programming language

1.根据命令行编译文件,然后执行二进制文件

(1)go运行命令开始:#go run main.go

(2)入口在源码文件//src/cmd/go/main.go  ,找main()函数

// init() 函数,是go的隐式调用的,即会在包引入就执行。这里会在main()运行前初始化一些go的基本命令 
fun init(){
  base.Go.Commands = []*base.Command{ //go基本命令定义
    ...
    work.CmdBuild,
    run.CmdRun, //go run 命令定义
    ...
  }
}


fun main(){
    _ = go11tag  //
   flag.Usage = base.Usage
   flag.Parse() //从os.Args[1:]解析输入的命令行参数,从切片1开始
   log.SetFlags(0)
   args := flag.Args()//args[0]就是go后面跟的参数,我们这里是run
   if len(args) < 1 {
      base.Usage()
   }
   //如果是get或help的命令,
   if args[0] == "get" || args[0] == "help" {
      if !modload.WillBeEnabled() {
         // Replace module-aware get with GOPATH get if appropriate.
         *modget.CmdGet = *get.CmdGet
      }
   }
   cfg.CmdName = args[0] // for error messages
   //如果是get或help的命令,
   if args[0] == "help" {
      help.Help(os.Stdout, args[1:])
      return
   }
   //检查GOPATH的设置是否正确,不正确就退出了
   ...
   
//核心是BigCmdLoop这段循环执行命令
BigCmdLoop:
   for bigCmd := base.Go; ; {//go 基本命令结构体
      for _, cmd := range bigCmd.Commands {//go 可用命令列表
         //前面我们知道args[0]=run,args[1]=main.go
         if cmd.Name() != args[0] { //如果命令匹配不上就执行下一个
            continue
         }
         //如果有类似go mod help foo 更多的命令
         if len(cmd.Commands) > 0 {
            bigCmd = cmd
            args = args[1:] //把子命令截取,如:args[0]=help
            if len(args) == 0 {
               help.PrintUsage(os.Stderr, bigCmd)
               base.SetExitStatus(2)
               base.Exit()
            }
            if args[0] == "help" {
               // Accept 'go mod help' and 'go mod help foo' for 'go help mod' and 'go help mod foo'.
               help.Help(os.Stdout, append(strings.Split(cfg.CmdName, " "), args[1:]...))
               return
            }
            //错误提示信息"build", "install", "list", "mod tidy", etc.
            cfg.CmdName += " " + args[0]
            continue BigCmdLoop
         }
         if !cmd.Runnable() {
            continue
         }
         //把命令参数传过去,调用具体方法
         invoke(cmd, args)
         base.Exit()
         return
      }
      helpArg := ""
      if i := strings.LastIndex(cfg.CmdName, " "); i >= 0 {
         helpArg = " " + cfg.CmdName[:i]
      }
      fmt.Fprintf(os.Stderr, "go %s: unknown command\nRun 'go help%s' for usage.\n", cfg.CmdName, helpArg)
      base.SetExitStatus(2)
      base.Exit()
   }
}


func invoke(cmd *base.Command, args []string) {
  ...
  //如go run main.go这个执行会真正执行到对应的实现函数去
  cmd.Run(ctx, cmd, args)//CmdRun.Run = runRun,实际上会调runRun方法(go build,CmdBuild.Run = runBuild)
  span.Done()
}
invoke()方法会真正执行到src/cmd/go/internal/run/run.go里面的runRun()方法(如果是go build main.go会找到src/cmd/go/internal/work/build.go的runBuild)

(3)源码文件://src/cmd/go/internal/run/run.go  ,找runRun()方法

var CmdRun = &base.Command{
  UsageLine: "go run [build flags] [-exec xprog] package [arguments...]",
  Short:     "compile and run Go program",
  Long: `Run compiles and runs the named main Go package...`,//Run编译并运行命名的主Go包...
}
func init() {
   
   CmdRun.Run = runRun // break init loop
   work.AddBuildFlags(CmdRun, work.DefaultBuildFlags)
   CmdRun.Flag.Var((*base.StringsFlag)(&work.ExecCmd), "exec", "")
}
//go run main.go最终会执行到这里
func runRun(ctx context.Context, cmd *base.Command, args []string) {
  //看run时有没有指定包,如go run cmd@version,有则导入包
  if shouldUseOutsideModuleMode(args) {
     // Set global module flags for 'go run cmd@version'.
     // This must be done before modload.Init, but we need to call work.BuildInit
     // before loading packages, since it affects package locations, e.g.,
     // for -race and -msan.
     modload.ForceUseModules = true
     modload.RootMode = modload.NoRoot
     modload.AllowMissingModuleImports()
     modload.Init()
  }
  //运行之前,先编译,根据机器将源码编译成对应可执行文件,大概过程
  //1.词法分析(编译器会将扫描到的词法单位(Lexemes)归类到常量、保留字、运算符等标记(Tokens)中)
  //2.语法分析(接收上一阶段生成的Tokens序列,基于特定编程语言的规则生成抽象语法树AST)
  //3.语义分析(语义分析阶段用来检查代码的语义一致性)
  //4.中间码生成(中间代码介是于高级语言和机器语言之间,具有跨平台特性)
  //5.代码优化(改进中间代码,删除不必要的代码)
  //6.机器码生成(基于中间码生成汇编代码,汇编器根据汇编代码生成目标文件,目标文件经过链接器处理最终生成可执行文件)
  //基本所有的高级语言到机器码的输出都需要编译这个过程
  work.BuildInit()
  var b work.Builder
  //初始化
  b.Init()
  ...
  //链接到可执行文件
  a1 := b.LinkAction(work.ModeBuild, work.ModeBuild, p)
  //执行,buildRunProgram:是执行二进制文件
  a := &work.Action{Mode: "go run", Func: buildRunProgram, Args: cmdArgs, Deps: []*work.Action{a1}}
  b.Do(ctx, a)
}
 可以看的run之前,是先进行打包的:work.BuildInit(),打包完就是进行链接执行了。

(4)打包work.BuildInit()

         (4-1)词法分析:编译器会将扫描到的词法单位(Lexemes)归类到常量、保留字、运算符等标记(Tokens)中

         (4-2)语法分析:接收上一阶段生成的Tokens序列,基于特定编程语言的规则生成抽象语法树AST

         (4-3)语义分析:语义分析阶段用来检查代码的语义一致性

         (4-4)中间码生成:中间代码介是于高级语言和机器语言之间,具有跨平台特性

         (4-5)代码优化:改进中间代码,删除不必要的代码)

         (4-6)机器码生成:基于中间码生成汇编代码,汇编器根据汇编代码生成目标文件,目标文件经过链接器处理最终生成可执行文件

(5)链接到可执行文件

     a1 := b.LinkAction(work.ModeBuild, work.ModeBuild, p)

(6)执行二进制文件

          buildRunProgram:是执行二进制文件

     a := &work.Action{Mode: "go run", Func: buildRunProgram, Args: cmdArgs, Deps: []*work.Action{a1}}

 到这里才开始执行二进制文件,才真正跑起main这个二进制文件。类似我们在服务器上启动:./main

(7)小结图解

​​​​​​​

2.引导程序执行,程序执行真正入口

(1)引导程序入口

go引导阶段,真正的程序入口在 src/runtime 包中,不同的计算机架构指向不同.如arm64的linux,对应的机器码就会找对应的入口

//src/runtime/rt0_linux_amd64.s

看名字很直观:

rt0:表示runtime0,

liunx:表示操作系统,

amd64:支持AMD64指令集,指处理器架构

平时我们打包程序:gf build main.go -n main -a amd64 -s linux -p  ,生成的机器码就是对应amd64指令集的。当我们执行时,操作系统就会引导程序来

(2)引导程序开始引导执行

   (2-1)src/runtime/rt0_linux_amd64.s 这里的入口开始执行。

#include "textflag.h"
TEXT _rt0_amd64_linux(SB),NOSPLIT,$-8
   JMP    _rt0_amd64(SB)
TEXT _rt0_amd64_linux_lib(SB),NOSPLIT,$0
   JMP    _rt0_amd64_lib(SB)
_rt0_amd64(SB)会找到//src/runtime/asm_amd64.s的TEXT _rt0_amd64(SB),NOSPLIT,$-8 方法

  (2-2)//src/runtime/asm_amd64.s ,引导执行的核心方法

TEXT _rt0_amd64(SB),NOSPLIT,$-8
   MOVQ   0(SP), DI  // argc
   LEAQ   8(SP), SI  // argv
   JMP    runtime·rt0_go(SB)
   
...
TEXT runtime·rt0_go(SB),NOSPLIT|TOPFRAME,$0
   // copy arguments forward on an even stack
   MOVQ   DI, AX    // argc
   MOVQ   SI, BX    // argv
   SUBQ   $(5*8), SP    // 3args 2auto
   ANDQ   $~15, SP
   MOVQ   AX, 24(SP)
   MOVQ   BX, 32(SP)
   // create istack out of the given (operating system) stack.
   // _cgo_init may update stackguard.
   //创建最初的G g0
   MOVQ   $runtime·g0(SB), DI
   LEAQ   (-64*1024+104)(SP), BX
   MOVQ   BX, g_stackguard0(DI)
   MOVQ   BX, g_stackguard1(DI)
   MOVQ   BX, (g_stack+stack_lo)(DI)
   MOVQ   SP, (g_stack+stack_hi)(DI)
  // find out information about the processor we're on
  MOVL   $0, AX
  CPUID
  CMPL   AX, $0
  JE nocpuinfo   
...
  
ok:
   // set the per-goroutine and per-mach "registers"
   //创建最初的线程 m0 和 goroutine g0,并把两者进行关联(g0.m = m0)
   get_tls(BX) //本地线程存储 (Thread Local Storage, TLS)
   LEAQ   runtime·g0(SB), CX
   MOVQ   CX, g(BX)
   LEAQ   runtime·m0(SB), AX
   // save m->g0 = g0
   MOVQ   CX, m_g0(AX)
   // save m0 to g0->m
   MOVQ   AX, g_m(CX)
  ...
  CALL    runtime·args(SB)//系统参数传递,主要是将系统参数转换传递给程序使用
  CALL   runtime·osinit(SB)//系统基本参数设置,主要是获取 CPU 核心数和内存物理页大小
  //各种运行时组件初始化工作.如调度器、内存分配器、堆、栈、GC 等。
  CALL   runtime·schedinit(SB)
  // create a new goroutine to start program
  //创建一个新的goroutine来启动程序,main()函数入口
  MOVQ   $runtime·mainPC(SB), AX       // entry
  PUSHQ  AX
  //创建一个运行fn的新goroutine。(GMP中的G)
  //把它放在等待运行的g队列中(goroutine调度队列,GMP中的p)
  //编译器将go语句转换为对this的调用。
  //会进行 p 的初始化,并将 m0 和某一个 p 进行绑定
  CALL   runtime·newproc(SB)
  POPQ   AX
  // start this M ,开启一个内核线程(GMP中的M),调用mstart0,调mstart1,调度器开始进行循环调度
  CALL   runtime·mstart(SB)
  CALL   runtime·abort(SB)  // mstart should never return

​​​​​​​

 

(2-3)m0和g0创建

   get_tls(BX)  //本地线程存储 (Thread Local Storage, TLS)
   LEAQ   runtime·g0(SB), CX
   MOVQ   CX, g(BX)
   LEAQ   runtime·m0(SB), AX
   // save m->g0 = g0
   MOVQ   CX, m_g0(AX)
   // save m0 to g0->m

     创建最初的线程 m0 和 goroutine g0,并把两者进行关联(g0.m = m0), g0 和 m0 是一组全局变量。

     m0 : m0 是 Go 启动时所创建的第一个系统线程,一个 Go 进程只有一个 m0,也叫主线程。

     g0:是m结构上的g指针,负责普通G在M上的调度切换。每个M都有自己的g0,即m0->g0,m1->g0...

type m struct {
   g0      *g // goroutine with scheduling stack
     ...

}
type g struct {
 stack       stack // offset known to runtime/cgo
 ...
 m         *m // current m; offset known to arm liblink
 sched     gobuf
...

}

   (2-4) 关于GMP:

     G-Goroutine:Go协程,是参与调度与执行的最小单位。普通G是执行用户任务的协程G。g0比较特殊,是执行调度普通G执行任务的。

     M-Machine:系统内核级别线程,m0是主线程,一个 Go 进程只有一个 m0,后续M由 Go Runtime 内自行创建。主线程m0在启动完初始化工作之后,会和普通M工作一样。

     P-Processor:是逻辑处理器,P关联了的本地可运行G的队列(也称为LRQ),最多可存放256个G

(2-5)g0,常规g,m0,常规M

m0和普通m:

      m0是主线程,一个 Go 进程只有一个 m0,后续M由 Go Runtime 内自行创建。主线程m0在启动完初始化工作之后,会和普通M工作一样。

g0和普通g:

g0 比较特殊,每一个 m 都只有一个 g0(仅此只有一个 g0),且每个 m 都只会绑定一个 g0。

A.数据结构一样

B.存在栈不同:g0栈分配的是系统栈,linux上栈默认大小是固定8M,不能扩缩。常规g起始只有2KB,可扩缩

C.运行状态不同:g0没那么多运行状态,也不会被调度程序抢占,调度本身就是在g0上运行的

(3) CALL runtime·schedinit(SB)各种运行时组件初始化工作

  (3-1)src/runtime/proc.go,启动核心的方法初始化如调度器、内存分配器、堆、栈、GC 等

func schedinit() {
  ...
  sched.maxmcount = 10000 //初始化机器线程数M最大为10000
  ...
  stackinit()//goroutine 执行栈初始化
  mallocinit()//内存分配器初始化
  initPageTrace(godebug)//内存页初始化
  ...
  modulesinit()   // 系统线程的部分初始化工作
  ...
  gcinit()//垃圾回收初始化
  ...
  procs := ncpu //P(go逻辑处理器)数量,gogetenv("GOMAXPROCS")
  procs := ncpu
  //GOMAXPROCS的数量一般和cup的核数相同,因为M和P是一一绑定的
  if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {
   procs = n
  }
  ...
  procresize(procs)
  ...
}

(4) CALLruntime·newproc(SB)会进行 p 的初始化,并将 m0 和某一个 p 进行绑定

  (4-1)src/runtime/proc.go,这里GMP中的三个关键结构就出现了

//创建一个运行fn的新goroutine。
func newproc(fn *funcval) {
   gp := getg()
   pc := getcallerpc()
   systemstack(func() {
      newg := newproc1(fn, gp, pc)
      pp := getg().m.p.ptr()
      runqput(pp, newg, true)
      if mainStarted {
         wakep()
      }
   })
}

(5) CALL runtime·mstart(SB)启动循环调度

  (5-1)src/runtime/proc.go,这里start this M ,开启一个内核线程(GMP中的M),调用mstart0,调mstart1,调度器开始进行循环调度

// mstart is the entry-point for new Ms.
// It is written in assembly, uses ABI0, is marked TOPFRAME, and calls mstart0.
func mstart()
//创建一个运行fn的新goroutine。
func mstart0() {
   gp := getg()
   osStack := gp.stack.lo == 0
   if osStack {
      // Initialize stack bounds from system stack.
      // Cgo may have left stack size in stack.hi.
      // minit may update the stack bounds.
      //
      // Note: these bounds may not be very accurate.
      // We set hi to &size, but there are things above
      // it. The 1024 is supposed to compensate this,
      // but is somewhat arbitrary.
      size := gp.stack.hi
      if size == 0 {
         size = 8192 * sys.StackGuardMultiplier
      }
      gp.stack.hi = uintptr(noescape(unsafe.Pointer(&size)))
      gp.stack.lo = gp.stack.hi - size + 1024
   }
   // Initialize stack guard so that we can start calling regular
   // Go code.
   gp.stackguard0 = gp.stack.lo + _StackGuard
   // This is the g0, so we can also call go:systemstack
   // functions, which check stackguard1.
   gp.stackguard1 = gp.stackguard0
   mstart1()
   // Exit this thread.
   if mStackIsSystemAllocated() {
      // Windows, Solaris, illumos, Darwin, AIX and Plan 9 always system-allocate
      // the stack, but put it in gp.stack before mstart,
      // so the logic above hasn't set osStack yet.
      osStack = true
   }
   //退出线程
   mexit(osStack)
}
//保存g0的调度信息
// The go:noinline is to guarantee the getcallerpc/getcallersp below are safe,
// so that we can set up g0.sched to return to the call of mstart1 above.
func mstart1() {
  ...
  schedule()
}
//调度器:找到一个可运行的goroutine并执行它。
func schedule() {
  ...
  execute(gp, inheritTime)
}
//调度gp在当前M上运行。
func execute(gp *g, inheritTime bool) {
  ...
}

(6)小结图解

 

到这里初步了解go执行过程,后续还有很多个点需要再细致了解,中间参考了很多优秀的文章。

参考资料:

知乎:mingguangtu的深入分析Go1.18 GMP调度器底层原理:深入分析Go1.18 GMP调度器底层原理 - 知乎

欧长坤创作的go语言原本 :Changkun Ou | Go 语言原本

tink.的GMP模型:https://go.cyub.vip/gmp/gmp-model.html#id11

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/128092.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于深度学习的人工林地面激光扫描点云立木特征参数提取方法

Abstract 利用基于三维点云的技术量化立木和立木参数&#xff0c;可以在林业生态效益评估和立木培育和利用中发挥关键作用。随着光探测与测距&#xff08;LiDAR&#xff09;扫描等三维信息获取技术的进步&#xff0c;可以更高效地获取大面积、复杂地形的树木林分信息。然而&am…

IFC常用关系定义

IFC常用关系定义 IfcRelDefinesByType IfcRelDefinesByType表示对象化的关系(The objectified relationship)&#xff0c;定义了一种对象类型定义(object type)和对象实体(object occurrences)的关系。IfcRelDefinesByType是1:n的关系&#xff0c;可以将一个对象类型定义(obj…

深入浅出scala之集合体系(集合即容器)(P46-4)

文章目录一、容器概念二、定长数组一、容器概念 1.集合是一种用来存储各种对象和数据的容器。 2.Scala集合分为可变和不可变的集合&#xff0c;不可变集合可以安全的并发访问。 可变集合可以在适当的地方被更新或扩展。这意味着可以修改&#xff0c;添加&#xff0c;移除一个集…

笔记:软件工程常用开源文档模板 + 软件著作权

https://github.com/AlexanderZhou01/China-software-copyright 下载以上的工程 解压放到U盘里 打开 D:\China-software-copyright-master 国家版权保护中心网址 办理步骤为企业获取政策优惠&#xff0c;减免。为学生提供成果。 办理步骤 (3030)∗50(3030)*50(3030)∗5…

Python 高效率传参调用 MATLAB 程序

Background python调用matlab脚本需要注意前置条件&#xff0c;具体参考官方文档&#xff1a;从 Python 中调用 MATLAB&#xff0c;大致就是两点&#xff1a;一是需要python和matlab版本对应&#xff0c;二是需要matlab运行环境mcr。具体安装配置可以参考&#xff1a;java和pyt…

使用Filebeat和AWS CloudWatch Logs将EC2上的Tomcat的access_log传送到ELK

文章目录背景和方案选择前提注册AWS账号创建EC2实例注意事项在EC2实例上安装aws-cloudwatch-agent注意事项测试aws-cloudwatch-agent是否可用使用Docker Compose部署ELK使用Docker Compose部署Filebeat配置文件说明docker-compose.yml说明filebeat配置文件说明input配置AWS Clo…

华为智能基座实验【计算机组成原理】

华为智能基座实验【计算机组成原理】前言推荐华为智能基座实验【计算机组成原理】1 课程介绍1.1 简介1.2 内容描述1.3 读者知识背景1.4 实验环境说明2 实验 1&#xff1a;hello-world 示例程序2.1 实验介绍2.1.1 关于本实验2.1.2 教学目标2.1.3 实验内容介绍2.2 实验任务操作指…

宝宝多大戒尿不湿?不看年龄看表现,用对方法轻松戒掉尿不湿

宝宝刚出生的时候&#xff0c;大小便次数比较多&#xff0c;宝宝自己也控制不了。这时&#xff0c;使用尿布可以减少父母的大量工作。但使用尿布只是暂时的。当宝宝到了一定年龄&#xff0c;就应该戒掉。宝宝能戒尿多大&#xff1f;既然尿布是用来兜宝宝大小便的&#xff0c;如…

从零学习 InfiniBand-network架构(十一) —— 管理器的介绍

从零学习 InfiniBand-network架构&#xff08;十一&#xff09; —— 管理器的介绍 &#x1f508;声明&#xff1a; &#x1f603;博主主页&#xff1a;王_嘻嘻的CSDN主页 &#x1f511;未经作者允许&#xff0c;禁止转载 &#x1f6a9;本专题部分内容源于《InfiniBand-network…

作为QA,我们要如何思考?

随着测试人员陆续开始尝试角色转变&#xff0c;坚守的QA需要找到自己的发展之路。兴趣和性格是客观因素&#xff0c;好奇心和发散性思维则是帮助成为优秀QA的必要因素。我想通过一些小的例子来与大家互动探讨。 测试你做对了吗&#xff1f; 让我们从这样一个现实中的小例子来…

【历史上的今天】12 月 30 日:C++ 之父诞生;Hotmail 创始人出生;Facebook 注册破百万

整理 | 王启隆 透过「历史上的今天」&#xff0c;从过去看未来&#xff0c;从现在亦可以改变未来。 今天是 2022 年 12 月 30 日&#xff0c;在 1930 年的这一天&#xff0c;“青蒿素之母”屠呦呦出生。2015 年 10 月 5 日&#xff0c;屠呦呦获得诺贝尔生理学或医学奖&#xf…

【Java基础知识】对Object类wait()和notify()方法的理解

wait()/notify()原理 当前线程必须拥有此对象的monitor监视器&#xff08;锁&#xff09;。&#xff08;不获取锁直接用Object对象调用wait和notify,会报错java.lang.IllegalMonitorStateException&#xff09;当前线程调用wait() 方法&#xff0c;线程就会释放此锁的所有权&a…

动态库和静态库

文章目录感性认识库动态库和静态库从库的设计者来看库制作静态库制作动态库库的使用者的角度静态库的使用使用动态库1.把头文件和库文件拷贝到系统的路径下2.修改对应的环境变量更改文件 ld.so.conf.d在系统的库路径下建立对应的软链接动态库如何加载感性认识库 首先&#xff…

关于GNSS关键性能测试,应该如何选择?

现在&#xff0c;GPS/GNSS信号无处不在&#xff0c;而GNSS接收机芯片的低成本和高性能也让在不同设备中安装GNSS接收机变得更为容易。然而&#xff0c;与此同时又存在一个问题&#xff0c;如果想将这些接收器芯片集成到一个设备或系统中&#xff0c;该如何对其进行全面的GNSS测…

方法注解@Bean与对象注入

1.类注解是添加到某个类上的&#xff0c;⽽⽅法注解是放到某个⽅法上的&#xff0c;如以下代码的实现&#xff1a; 注&#xff1a;方法注解一定要搭配类注解使用&#xff0c;否则会报错 2.重命名Bean 多个重命名&#xff0c;使用多个名称获取的对象都是同一个 3.获取 Bean 对象…

全网惟一面向软件测试人员的Python基础教程-在Python中列表是什么?

全网惟一面向软件测试人员的Python基础教程 起点&#xff1a;《python软件测试实战宝典》介绍 第一章 为什么软件测试人员要学习Python 第二章 学Python之前要搞懂的道理 第三章 你知道Python代码是怎样运行的吗&#xff1f; 第四章 Python数据类型中有那些故事呢&#xff1f;…

TTL(RGB)接口液晶显示屏的调试方法

TTL接口的液晶显示屏一般会使用DE模式驱动它。首先需要CPU带有LCD控制器&#xff0c;能够产生出液晶显示屏所需要的以下几个信号&#xff1a; 1.时钟信号(DOTCLK) 2.行同步信号(HSYNC) 3.场同步信号(VSYNC) 4.DEN(数据允许信号) 6.数据信号&#xff08;R0-R7;G0-G7;B0-B7)…

第02讲:Git分支操作

一、什么是分支 在版本控制过程中&#xff0c;同时推进多个任务&#xff0c;为每个任务&#xff0c;我们就可以创建每个任务的单独 分支。使用分支意味着程序员可以把自己的工作从开发主线上分离开来&#xff0c;开发自己分支的时 候&#xff0c;不会影响主线分支的运行。对于…

剑指 Offer 17. 打印从1到最大的n位数

题目 输入数字 n&#xff0c;按顺序打印出从 1 到最大的 n 位十进制数。比如输入 3&#xff0c;则打印出 1、2、3 一直到最大的 3 位数 999。 思路 本题应该考虑的是大数问题&#xff0c;但是返回数组为int[]。。。所以两种方法都做一下 方法一&#xff1a;普通解法&#xff…

天翼云服务器性能评测,4H8G贵阳节点性能跑分

天翼云号称全球运营商云第一、中国公有云用户数第二、政务云公有云第一。那么天翼云服务器用起来到底怎么样呢&#xff1f; 目前&#xff0c;蓝队云这边一共有19个天翼云节点云服务器&#xff0c;覆盖全国多个核心省市及地区&#xff0c;节点选择的话一般就是按照就近原则。 …