掌握Go语言`runtime`包:性能优化与实战指南

devtools/2024/10/22 13:15:00/

掌握Go语言`runtime`包:性能优化与实战指南

    • 引言
    • 第一部分:初识`runtime`包
    • 第二部分:常用功能详解
    • 第三部分:实战技巧
      • 性能优化
        • Goroutine池管理
        • 内存调优
      • 调试技巧
        • 使用`runtime`包中的函数进行调试
      • 实战示例
        • 创建高效的并发程序
        • 内存监控和调优示例
    • 第四部分:深入理解`runtime`源码
      • `runtime`包的内部结构
      • 常用函数的源码解析
      • `runtime`包的内部机制
        • 调度器
        • 内存管理
      • 源码解析示例
    • 第五部分:常见问题与解决方案
      • 常见错误及其排查方法
        • Goroutine泄漏
        • 内存泄漏
      • 最佳实践
        • 合理使用Goroutine
        • 优化内存使用
        • 调试和诊断
    • 结论
      • 关键点总结

在这里插入图片描述

引言

在Go语言(Golang)中,runtime包是一个非常关键的标准库,它提供了与Go程序运行时系统交互的基本功能。这些功能包括管理goroutine、控制内存分配和垃圾回收、获取系统信息等。理解和善用runtime包可以帮助开发者更好地优化和调试Go程序,使其运行更加高效和稳定。

本篇文章将深入探讨runtime包的使用方法和技巧,通过丰富的代码示例和详细的讲解,帮助读者全面掌握这一工具包的强大功能。我们不会涉及Go语言的历史背景或安装过程,而是聚焦于实战开发,适合中高级开发者阅读和参考。

接下来,我们将从runtime包的基本概念入手,逐步介绍其核心功能、常见使用场景、实战技巧,并通过源码解析加深对其内部机制的理解。最后,我们还会列出一些常见问题和解决方案,帮助读者在实际开发中迅速定位并解决问题。

runtime_12">第一部分:初识runtime

runtime_14">runtime包概述

runtime包是Go语言标准库中的一个重要组成部分,负责管理Go程序的运行时行为。它提供了一组函数和变量,使开发者可以控制和监控程序的执行状态。了解并善用这些功能,对于编写高效且可靠的Go程序至关重要。

runtime_18">runtime包的核心功能

runtime包主要涵盖以下几个核心功能:

  1. Goroutine管理:Goroutine是Go语言中实现并发的重要机制。runtime包提供了一些函数用于管理和调度goroutine,包括退出当前goroutine、让出当前goroutine的执行权、获取当前正在运行的goroutine数量等。

  2. 内存管理:内存管理是runtime包的另一个关键功能。它包括垃圾回收(GC)、内存统计等。通过这些功能,开发者可以了解程序的内存使用情况,进行内存调优,提升程序的性能。

  3. 系统信息runtime包还提供了一些函数用于获取系统级别的信息,比如CPU的数量、Go的版本信息等。这些信息对于优化程序性能和调试非常有用。

第二部分:常用功能详解

Goroutine管理

runtimeGoexit_33">runtime.Goexit

runtime.Goexit函数用于立即终止当前的goroutine。它不会影响其他正在运行的goroutine,并且不会执行当前goroutine的defer语句。

package mainimport ("fmt""runtime"
)func main() {go func() {defer fmt.Println("This will not be printed.")fmt.Println("Exiting goroutine.")runtime.Goexit()fmt.Println("This will not be printed either.")}()runtime.Gosched() // 让出时间片,等待goroutine执行
}

在上面的示例中,调用runtime.Goexit后,当前goroutine立即退出,后续的defer语句和代码都不会执行。

runtimeGosched_58">runtime.Gosched

runtime.Gosched函数用于让出当前goroutine的执行权,允许其他goroutine运行。它不会挂起当前goroutine,也不会结束它,只是简单地将它放回队列,等待下次调度。

package mainimport ("fmt""runtime"
)func main() {go func() {for i := 0; i < 5; i++ {fmt.Println("Goroutine iteration:", i)runtime.Gosched()}}()for i := 0; i < 5; i++ {fmt.Println("Main iteration:", i)runtime.Gosched()}
}

在这个示例中,runtime.Gosched用于让出当前goroutine的执行权,使主goroutine和子goroutine能够交替运行。

runtimeNumGoroutine_86">runtime.NumGoroutine

runtime.NumGoroutine函数返回当前正在运行的goroutine的数量。这对于监控程序的并发度非常有帮助。

package mainimport ("fmt""runtime"
)func main() {fmt.Println("Number of goroutines:", runtime.NumGoroutine())go func() {fmt.Println("Number of goroutines inside goroutine:", runtime.NumGoroutine())}()runtime.Gosched() // 让子goroutine有机会运行fmt.Println("Number of goroutines after launch:", runtime.NumGoroutine())
}

运行上述代码,你会看到程序在不同阶段的goroutine数量,帮助你了解程序的并发情况。

内存管理

runtimeMemStats_113">runtime.MemStats

runtime.MemStats结构体用于存储内存统计信息。通过调用runtime.ReadMemStats函数,可以获取当前的内存使用情况,并填充到runtime.MemStats结构体中。

以下是一个示例,展示如何使用runtime.MemStatsruntime.ReadMemStats获取内存统计信息:

package mainimport ("fmt""runtime"
)func printMemStats() {var memStats runtime.MemStatsruntime.ReadMemStats(&memStats)fmt.Printf("Alloc = %v MiB\n", memStats.Alloc / 1024 / 1024)fmt.Printf("TotalAlloc = %v MiB\n", memStats.TotalAlloc / 1024 / 1024)fmt.Printf("Sys = %v MiB\n", memStats.Sys / 1024 / 1024)fmt.Printf("NumGC = %v\n", memStats.NumGC)
}func main() {printMemStats()
}

在这个示例中,printMemStats函数调用runtime.ReadMemStats获取当前内存使用情况,并打印出几项关键的内存统计数据。

runtimeGC_143">runtime.GC

runtime.GC函数用于触发一次垃圾回收。通常情况下,Go的垃圾回收器会自动运行,但在某些场景下,手动触发垃圾回收可能有助于内存管理和性能调优。

以下是一个示例,展示如何使用runtime.GC触发垃圾回收:

package mainimport ("fmt""runtime"
)func main() {var memStats runtime.MemStats// 分配一些内存for i := 0; i < 10; i++ {_ = make([]byte, 10*1024*1024) // 分配10MB}runtime.ReadMemStats(&memStats)fmt.Printf("Before GC: Alloc = %v MiB\n", memStats.Alloc / 1024 / 1024)runtime.GC() // 手动触发垃圾回收runtime.ReadMemStats(&memStats)fmt.Printf("After GC: Alloc = %v MiB\n", memStats.Alloc / 1024 / 1024)
}

在这个示例中,我们在分配了一些内存后手动触发垃圾回收,并观察内存使用情况的变化。

系统信息

runtimeGOMAXPROCS_179">runtime.GOMAXPROCS

runtime.GOMAXPROCS函数用于设置和获取可同时执行的最大CPU数。这对于优化程序的并行性能非常重要。

以下是一个示例,展示如何使用runtime.GOMAXPROCS设置最大可用CPU数:

package mainimport ("fmt""runtime"
)func main() {numCPU := runtime.NumCPU()fmt.Printf("Number of CPUs: %d\n", numCPU)// 设置最大可用CPU数runtime.GOMAXPROCS(numCPU)fmt.Printf("GOMAXPROCS set to: %d\n", runtime.GOMAXPROCS(0))
}

在这个示例中,我们首先获取系统的CPU数量,然后将可同时执行的最大CPU数设置为系统的CPU数量。

runtimeNumCPU_205">runtime.NumCPU

runtime.NumCPU函数返回当前系统的CPU数量。这对于了解系统资源和优化程序性能非常有用。

package mainimport ("fmt""runtime"
)func main() {numCPU := runtime.NumCPU()fmt.Printf("Number of CPUs: %d\n", numCPU)
}

在这个简单的示例中,我们打印出系统的CPU数量。

runtimeVersion_225">runtime.Version

runtime.Version函数返回Go的版本信息。这在调试和记录日志时可能非常有用。

package mainimport ("fmt""runtime"
)func main() {fmt.Printf("Go version: %s\n", runtime.Version())
}

在这个示例中,我们打印出当前使用的Go版本。

第三部分:实战技巧

性能优化

Goroutine池管理

在高并发程序中,合理管理goroutine的数量和生命周期可以显著提升程序的性能和稳定性。Goroutine池是一种常见的优化手段,用于限制同时运行的goroutine数量,避免资源耗尽。

以下是一个简单的goroutine池示例:

package mainimport ("fmt""sync""time"
)// Worker 函数,模拟执行任务
func worker(id int, wg *sync.WaitGroup) {defer wg.Done()fmt.Printf("Worker %d starting\n", id)time.Sleep(time.Second)fmt.Printf("Worker %d done\n", id)
}func main() {const numWorkers = 5const numTasks = 10var wg sync.WaitGrouptaskCh := make(chan int, numTasks)// 启动固定数量的worker goroutinesfor i := 1; i <= numWorkers; i++ {wg.Add(1)go func(id int) {defer wg.Done()for task := range taskCh {worker(task, &wg)}}(i)}// 发送任务到任务通道for i := 1; i <= numTasks; i++ {taskCh <- i}close(taskCh)wg.Wait()
}

在这个示例中,我们创建了一个任务通道,并启动了固定数量的worker goroutines。任务通过通道发送到worker,worker处理任务并记录日志。

内存调优

在Go程序中,内存管理是性能优化的关键。以下是一些常见的内存调优技巧:

  1. 避免内存泄漏:使用runtime.MemStats监控内存使用情况,定期调用runtime.GC触发垃圾回收。
  2. 预分配内存:对于已知大小的数据结构,可以使用make函数预分配内存,避免频繁的内存分配和回收。
  3. 合理使用sync.Poolsync.Pool是一种高效的对象池,可以重复利用已分配的对象,减少内存分配和垃圾回收的开销。

以下是一个使用sync.Pool的示例:

package mainimport ("fmt""sync"
)func main() {var pool = sync.Pool{New: func() interface{} {return new(string)},}str1 := pool.Get().(*string)*str1 = "Hello, World!"fmt.Println(*str1)pool.Put(str1)str2 := pool.Get().(*string)fmt.Println(*str2) // str2指向的对象已经被复用
}

在这个示例中,我们使用sync.Pool实现了一个字符串对象池,避免了频繁的内存分配和回收。

调试技巧

runtime_340">使用runtime包中的函数进行调试

runtime包提供了一些调试和诊断功能,可以帮助开发者更好地理解和优化程序。以下是几个常用的调试技巧:

  1. 获取调用栈信息:使用runtime.Callersruntime.FuncForPC获取当前goroutine的调用栈信息。
  2. 监控goroutine数量:使用runtime.NumGoroutine监控当前正在运行的goroutine数量,避免goroutine泄漏。

以下是一个示例,展示如何获取调用栈信息:

package mainimport ("fmt""runtime"
)func printStack() {pc := make([]uintptr, 10)n := runtime.Callers(0, pc)frames := runtime.CallersFrames(pc[:n])for {frame, more := frames.Next()fmt.Printf("%s\n\t%s:%d\n", frame.Function, frame.File, frame.Line)if !more {break}}
}func main() {printStack()
}

在这个示例中,我们使用runtime.Callersruntime.CallersFrames获取并打印当前的调用栈信息。

实战示例

创建高效的并发程序

以下是一个高效并发HTTP服务器的示例,展示如何使用runtime包优化并发性能:

package mainimport ("fmt""net/http""runtime""sync"
)func handler(w http.ResponseWriter, r *http.Request) {fmt.Fprintf(w, "Hello, World!")
}func main() {runtime.GOMAXPROCS(runtime.NumCPU()) // 设置最大并发CPU数http.HandleFunc("/", handler)fmt.Println("Starting server on :8080")if err := http.ListenAndServe(":8080", nil); err != nil {fmt.Println("Error starting server:", err)}
}

在这个示例中,我们使用runtime.GOMAXPROCS设置最大并发CPU数,优化HTTP服务器的并发性能。

内存监控和调优示例

以下是一个示例,展示如何监控和调优Go程序的内存使用情况:

package mainimport ("fmt""runtime""time"
)func allocateMemory() {for i := 0; i < 10; i++ {_ = make([]byte, 10*1024*1024) // 分配10MBtime.Sleep(time.Second)}
}func main() {var memStats runtime.MemStatsgo allocateMemory()for i := 0; i < 15; i++ {runtime.ReadMemStats(&memStats)fmt.Printf("Alloc = %v MiB\n", memStats.Alloc / 1024 / 1024)time.Sleep(time.Second)}
}

在这个示例中,我们创建了一个goroutine不断分配内存,并在主goroutine中定期读取和打印内存使用情况。

runtime_447">第四部分:深入理解runtime源码

runtime_449">runtime包的内部结构

为了更好地理解runtime包的工作原理,我们需要深入研究其内部结构和实现细节。runtime包的源码位于Go语言的源码仓库中,它主要由以下几个部分组成:

  1. 调度器:负责管理goroutine的创建、调度和销毁。
  2. 内存管理:包括垃圾回收器(GC)和内存分配器。
  3. 系统调用和操作:提供与操作系统交互的功能,比如获取系统信息、设置线程数等。
  4. 调试和诊断:提供获取调用栈信息、监控运行时状态等功能。

常用函数的源码解析

runtimeGOMAXPROCS_460">runtime.GOMAXPROCS

runtime.GOMAXPROCS函数用于设置和获取可同时执行的最大CPU数。它的源码实现如下:

// GOMAXPROCS sets the maximum number of CPUs that can be executing
// simultaneously and returns the previous setting. If n < 1, it does not change the current setting.
func GOMAXPROCS(n int) int {if n <= 0 {return int(gomaxprocs)}lock(&sched.lock)ret := int(gomaxprocs)if n > _MaxGomaxprocs {n = _MaxGomaxprocs}gomaxprocs = int32(n)procs := gomaxprocs - retif procs > 0 {needaddg += int(procs)ready(nil, 0)}unlock(&sched.lock)return ret
}

从源码中可以看到,GOMAXPROCS函数首先获取当前设置的最大CPU数,如果传入的参数n大于0,则更新gomaxprocs变量,并根据需要调整线程数量。

runtimeGoexit_489">runtime.Goexit

runtime.Goexit函数用于终止当前的goroutine。它的源码实现如下:

// Goexit terminates the currently running goroutine. No other goroutine is affected.
// Goexit runs all deferred calls before terminating the goroutine.
func Goexit() {mcall(goexit)
}// goexit terminates the currently running goroutine.
// It runs all deferred functions and then gets called by mcall.
func goexit1() {g := getg()if g.m.curg != g {throw("bad g in goexit")}if raceenabled {racegoend()}if trace.enabled {traceGoSched()}// Run all deferred functions.for {d := g._deferif d == nil {break}g._defer = d.linkfn := d.fnd.fn = nilreflectcall(nil, unsafe.Pointer(fn), unsafe.Pointer(&d._panic), uint32(d.siz), uint32(d.siz))}g.m.curg = nilg.status = _Gdeadschedule()
}

Goexit函数中,通过调用mcall(goexit)来终止当前goroutine,并运行所有的defer函数。goexit1函数具体实现了这些操作。

runtime_532">runtime包的内部机制

调度器

Go的调度器采用了M:N调度模型,即多个goroutine可以映射到多个操作系统线程上执行。调度器的核心结构包括:

  • G(Goroutine):表示一个goroutine。
  • M(Machine):表示一个操作系统线程。
  • P(Processor):表示一个逻辑处理器,负责调度和执行G。

以下是调度器的关键结构体:

type g struct {// ... 其他字段省略
}type m struct {// ... 其他字段省略
}type p struct {// ... 其他字段省略
}

调度器通过M和P来管理和调度G,确保高效的并发执行。

内存管理

Go的内存管理主要依赖垃圾回收器(GC)。GC采用了并发标记-清除算法,能够在程序运行时进行垃圾回收,而不会导致长时间的暂停。GC的主要步骤包括:

  1. 标记:标记所有活动对象。
  2. 清除:清除未被标记的对象,回收内存。

以下是GC的关键代码:

func gcMarkDone() {// ... 省略
}func gcSweep() {// ... 省略
}

源码解析示例

为了更好地理解runtime包的工作原理,我们以runtime.GC函数为例进行详细解析:

// GC runs a garbage collection and blocks the caller until the
// garbage collection is complete. It may also block the entire
// program.
func GC() {runtime.GC()
}

runtime.GC函数触发了一次垃圾回收,并等待其完成。它内部调用了runtime包的GC实现,完成标记和清除工作。

第五部分:常见问题与解决方案

常见错误及其排查方法

在使用runtime包时,开发者可能会遇到各种错误和问题。以下是一些常见错误及其排查方法:

Goroutine泄漏

问题描述:Goroutine泄漏是指程序中创建了大量未终止的goroutine,占用系统资源,导致性能下降甚至程序崩溃。

解决方案

  1. 监控goroutine数量:使用runtime.NumGoroutine监控程序中的goroutine数量,及时发现异常增加的情况。
  2. 避免无休止的goroutine:确保goroutine在合适的条件下终止,避免无休止的循环或阻塞。
  3. 使用context管理goroutine生命周期:使用context包管理goroutine的生命周期,确保在超时或取消时正确终止。

示例代码:

package mainimport ("context""fmt""runtime""time"
)func worker(ctx context.Context) {for {select {case <-ctx.Done():fmt.Println("Worker done")returndefault:fmt.Println("Working")time.Sleep(time.Second)}}
}func main() {ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)defer cancel()go worker(ctx)time.Sleep(10 * time.Second)fmt.Printf("Number of goroutines: %d\n", runtime.NumGoroutine())
}

在这个示例中,我们使用context包来管理goroutine的生命周期,确保在超时后正确终止goroutine。

内存泄漏

问题描述:内存泄漏是指程序中分配的内存未能及时释放,导致内存占用不断增加,最终可能导致程序崩溃。

解决方案

  1. 定期触发垃圾回收:使用runtime.GC定期触发垃圾回收,释放未使用的内存。
  2. 监控内存使用情况:使用runtime.MemStats监控内存使用情况,及时发现内存泄漏。
  3. 合理使用对象池:使用sync.Pool复用对象,减少内存分配和回收的开销。

示例代码:

package mainimport ("fmt""runtime""time"
)func allocateMemory() {for i := 0; i < 10; i++ {_ = make([]byte, 10*1024*1024) // 分配10MBtime.Sleep(time.Second)}
}func main() {var memStats runtime.MemStatsgo allocateMemory()for i := 0; i < 15; i++ {runtime.ReadMemStats(&memStats)fmt.Printf("Alloc = %v MiB\n", memStats.Alloc / 1024 / 1024)runtime.GC() // 定期触发垃圾回收time.Sleep(time.Second)}
}

在这个示例中,我们定期触发垃圾回收,并监控内存使用情况,及时发现和解决内存泄漏问题。

最佳实践

合理使用Goroutine

在Go语言中,goroutine是实现并发的重要机制,但滥用goroutine可能导致资源浪费和性能问题。以下是一些最佳实践:

  1. 限制goroutine数量:使用goroutine池或限流机制,控制同时运行的goroutine数量。
  2. 避免长时间阻塞:确保goroutine不会长时间阻塞,避免资源浪费。
  3. 使用defer释放资源:在goroutine中使用defer语句及时释放资源,确保goroutine终止时清理必要的状态。
优化内存使用

内存管理对于高性能Go程序至关重要。以下是一些优化内存使用的最佳实践:

  1. 预分配内存:对于已知大小的数据结构,使用make函数预分配内存,减少运行时的内存分配和回收开销。
  2. 使用对象池:对于频繁创建和销毁的对象,使用sync.Pool复用对象,减少垃圾回收的压力。
  3. 监控内存使用情况:定期使用runtime.MemStats监控内存使用情况,及时发现和解决内存问题。
调试和诊断

在开发和调试Go程序时,合理使用runtime包提供的调试和诊断功能,可以帮助你快速定位和解决问题。

  1. 获取调用栈信息:使用runtime.Callersruntime.FuncForPC获取调用栈信息,定位程序中的问题。
  2. 监控运行时状态:使用runtime.NumGoroutineruntime.ReadMemStats等函数监控程序的运行时状态,及时发现异常情况。
  3. 定期触发垃圾回收:在开发和调试过程中,定期使用runtime.GC触发垃圾回收,观察内存使用情况的变化。

结论

在本文中,我们深入探讨了Go语言标准库中的runtime包,详细介绍了其核心功能、实战技巧以及内部机制。通过丰富的代码示例,我们展示了如何使用runtime包来管理goroutine、优化内存、获取系统信息等。

关键点总结

  1. Goroutine管理:通过runtime.Goexitruntime.Goschedruntime.NumGoroutine等函数,可以有效地管理和监控goroutine的运行状态,避免goroutine泄漏和资源浪费。

  2. 内存管理:使用runtime.MemStats获取内存统计信息,定期触发runtime.GC进行垃圾回收,并合理使用对象池,可以显著优化程序的内存使用,避免内存泄漏。

  3. 系统信息获取:通过runtime.GOMAXPROCSruntime.NumCPUruntime.Version等函数,可以获取和设置系统级别的信息,优化程序的并发性能和兼容性。

  4. 实战技巧:通过创建goroutine池、优化内存使用和使用runtime包中的调试功能,可以提升程序的性能和可靠性。

  5. 源码解析:深入理解runtime包的源码和内部机制,可以帮助开发者更好地掌握其工作原理,从而编写出更高效、更可靠的Go程序。

随着Go语言的不断发展和演进,runtime包也在不断优化和完善。未来,Go语言可能会引入更多的并发和内存管理特性,进一步提升程序的性能和开发效率。因此,持续关注和学习runtime包的新特性和最佳实践,对于Go开发者来说是非常重要的。

通过本篇文章的学习,希望你对runtime包有了更加深入的了解,并能够在实际开发中灵活运用这些知识和技巧,编写出高性能、高可靠性的Go程序。


http://www.ppmy.cn/devtools/127839.html

相关文章

python 爬虫 入门 四、线程,进程,协程

线程和进程大部分人估计都知道&#xff0c;但协程就不一定了。 一、进程 进程是操作系统分配资源和调度的基本单位&#xff0c;一个程序开始运行时&#xff0c;操作系统会给他分配一块独立的内存空间并分配一个PCB作为唯一标识。初始化内存空间后进程进入就绪态&#xff0c;PC…

关于Qt中进行输出的方式及对比分析

本文详细介绍了在Qt框架和C标准库中可用的多种输出流方式&#xff0c;主要分为标准C输出和Qt输出两大类。 摘要 标准C输出 std::cout&#xff1a;用于标准输出&#xff0c;支持多种数据类型&#xff0c;是C最常用的输出流。结合std::string、QString&#xff08;需转换&#…

迅为RK3562开发板/核心板240PIN引脚全部引出,产品升级自如

可应用于人脸跟踪、身体跟踪、视频监控、自动语音识别(ASR)、图像分类驾驶员辅助系统(ADAS)、车牌识别、物体识别等。 iTOP-3562开发板/核心板采用瑞芯微RK3562处理器&#xff0c;内部集成了四核A53Mali G52架构&#xff0c;主频2GHZ&#xff0c;内置1TOPSNPU算力&#xff0c;R…

docker/docker-compose里面Command和entrypoint的关系

在Docker中&#xff0c;ENTRYPOINT和CMD都是用于指定容器启动时要执行的命令或程序的关键指令。它们之间的关系如下&#xff1a; 1. **ENTRYPOINT**&#xff1a; - ENTRYPOINT用于指定容器启动时要执行的主要命令或程序。它可以设置容器的主要可执行文件&#xff0c;在运行…

为什么要做PFAS测试?PFAS检测项目详细介绍

PFAS测试之所以重要&#xff0c;主要归因于PFAS&#xff08;全氟和多氟化合物&#xff09;的广泛存在、持久性、生物累积性和潜在的毒性。这些特性使得PFAS在环境和人体中可能长期存在&#xff0c;并对生态系统和人类健康构成威胁。以下是对PFAS检测项目的详细介绍以及进行PFAS…

关于懒汉饿汉模式下的线程安全问题

1.java标准库中的线程安全 java标准库中有很多的都是线程不安全的&#xff0c;这些类可能会涉及到多线程修改共享数据&#xff0c;又没有任何加锁措施&#xff0c;如下所示&#xff1a; 上面的这些类中都没有进行任何的加锁限制&#xff0c;因此线程不安全&#xff0c;但是还有…

Maplibre-gl\Mapbox-gl改造支持对矢量瓦片加密

Maplibre-gl是Mapbox-gl剔除自带地图服务之后的一个分支,代码很相似。Maplibre-gl\Mapbox-gl使用的pbf格式的矢量瓦片,数据量小,渲染效果好。但也存在着信息泄露的风险。但如果想使用这个开发框架的前端渲染效果,还必须要使用这个格式。最近研究了一下如何对矢量瓦片进行加…

【国产化信创平台】Linux系统设置程序 开机自启

目录 一、创建编辑自启服务文件 步骤1&#xff1a;创建服务文件 步骤2&#xff1a;编辑服务文件 步骤3&#xff1a;重新加载systemd并启用服务 二、自启动展示​编辑 一、创建编辑自启服务文件 要在Linux系统中配置一个开机启动服务&#xff0c;你需要创建一个 systemd 服…