【8】深入理解 Go 语言中的协程-从基础到高级应用

文章目录

    • 一、引言 🌟
    • 二、协程基础概念 🧐
    • (一)什么是协程
    • (二)协程与线程、进程的区别
    • 三、协程的创建与启动 🚀
    • (一)使用 go 关键字创建协程
    • (二)简单的协程示例代码
    • 四、协程间通信 📡
    • (一)通道(Channel)的概念与作用
    • (二)通道的创建与使用
    • (三)使用通道在协程间传递数据
    • 五、协程的同步与互斥 🔒
    • (一)互斥锁(Mutex)的使用场景
    • (二)使用 WaitGroup 实现协程同步
    • 六、协程的生命周期管理 🌱
    • (一)如何优雅地结束协程
    • (二)处理协程中的错误
    • 七、协程的性能优势 💪
    • (一)对比传统线程模型的性能提升
    • (二)在高并发场景下的表现
    • 八、实际应用案例 🛠️
    • (一)Web 服务器中的协程应用
    • (二)数据处理任务中的协程使用

一、引言 🌟

在当今的软件开发世界中,并发编程已经成为一项必不可少的技能,尤其是在处理高并发场景和大规模数据处理时。Go 语言作为一门强大的编程语言,其协程(Goroutines)机制是其并发编程的核心优势之一。协程在 Go 语言中的重要地位就如同魔法棒,让开发者能够轻松地编写出高效、简洁且并发性能卓越的程序。它允许我们同时处理多个任务,就像一个魔法师同时操控多个魔法咒语一样,极大地提高了程序的执行效率和资源利用率,是构建高性能应用程序的关键所在。

二、协程基础概念 🧐

(一)什么是协程

协程是 Go 语言中的轻量级线程,是 Go 运行时环境管理的并发执行单元。它们在 Go 程序中独立运行,并且由 Go 运行时调度器负责调度,而非操作系统。可以将协程看作是一个函数的执行过程,它可以与其他协程同时运行,而不会阻塞程序的主线程。协程的创建和销毁开销极小,因此我们可以创建成千上万个协程而无需担心资源耗尽,这是传统线程所无法比拟的。

想象一下,你正在举办一场盛大的音乐会,每个音乐家(协程)都可以在舞台上尽情演奏自己的乐器,而不需要等待其他音乐家演奏完毕。每个音乐家可以随时开始、暂停或结束自己的演奏,这就是协程在程序中的工作方式。

(二)协程与线程、进程的区别

进程

  • 进程是操作系统进行资源分配和调度的基本单位,拥有独立的内存空间、文件句柄等资源。启动一个进程会消耗大量的系统资源,包括内存和 CPU 时间。例如,启动一个新的进程可能需要分配新的内存页表、初始化进程控制块等,开销较大。可以用 🖥️ 图标来表示进程。

线程

  • 线程是进程的一部分,共享进程的资源,如内存空间。一个进程可以包含多个线程,它们可以并发执行,但操作系统对线程的调度开销仍然相对较大,尤其是在频繁创建和销毁线程时,因为涉及到内核态和用户态的切换。可以用 🔗 图标来表示线程。

协程

  • 协程是更轻量级的执行单元,运行在用户态,由 Go 运行时调度器调度。协程的栈空间非常小,通常只有几 KB,而线程的栈空间可能需要 MB 级别的内存。协程之间的切换由 Go 运行时管理,切换开销极小,这使得 Go 程序可以创建大量协程。可以用 🚀 图标来表示协程。

以下是一个简单的代码示例,展示了协程和线程在 Go 语言中的使用区别:

package main

import (
	"fmt"
	"sync"
	"time"
)

// 模拟一个长时间运行的任务
func longTask(id int) {
	for i := 0; i < 5; i++ {
		fmt.Printf("Task %d: %d\n", id, i)
		time.Sleep(100 * time.Millisecond)
	}
}

func main() {
	// 线程的使用(使用 sync.WaitGroup 来等待多个线程完成)
	var wg sync.WaitGroup
	wg.Add(2)
	go func() {
		defer wg.Done()
		longTask(1)
	}()
	go func() {
		defer wg.Done()
		longTask(2)
	}()
	wg.Wait()

	// 协程的使用
	for i := 3; i <= 4; i++ {
		go longTask(i)
	}
	time.Sleep(1 * time.Second)
}

在上述代码中,我们使用 sync.WaitGroup 来等待两个使用 go 关键字创建的协程(模拟线程)完成,然后使用 go 关键字创建另外两个协程。可以看到,协程的创建和使用更加简洁,不需要额外的等待机制,因为它们的生命周期通常由程序逻辑控制。

三、协程的创建与启动 🚀

(一)使用 go 关键字创建协程

使用 go 关键字是创建协程最基本的方法。当我们在函数调用前添加 go 关键字时,Go 运行时会将该函数作为一个协程启动。例如:

package main

import (
	"fmt"
	"time"
)

func printHello() {
	fmt.Println("Hello from Goroutine!")
	time.Sleep(1 * time.Second)
}

func main() {
	// 创建并启动一个协程
	go printHello()
	fmt.Println("Hello from Main!")
	time.Sleep(2 * time.Second)
}

在这个示例中,go printHello() 这行代码创建并启动了一个协程,该协程会调用 printHello 函数。printHello 函数会打印一条消息并睡眠 1 秒。注意,main 函数中的 time.Sleep(2 * time.Second) 是为了防止程序在协程完成之前退出,因为一旦 main 函数结束,程序会终止,所有的协程也会随之终止。

(二)简单的协程示例代码

让我们来看一个更复杂的示例,同时启动多个协程:

package main

import (
	"fmt"
	"sync"
	"time"
)

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() {
	var wg sync.WaitGroup
	for i := 1; i <= 5; i++ {
		wg.Add(1)
		go worker(i, &wg)
	}
	wg.Wait()
	fmt.Println("All workers done")
}

在这个示例中:

  • sync.WaitGroup 用于等待所有协程完成任务。可以用 ⏳ 图标表示等待。
  • worker 函数接收一个 idwg 指针作为参数,defer wg.Done() 确保在函数结束时通知 WaitGroup 该协程已完成任务。
  • wg.Add(1) 增加 WaitGroup 的计数,表示有一个新的协程正在运行。
  • go worker(i, &wg) 创建并启动协程。
  • wg.Wait() 会阻塞 main 函数,直到 WaitGroup 的计数为 0,即所有协程都完成任务。

四、协程间通信 📡

(一)通道(Channel)的概念与作用

通道是协程间通信的主要方式,它是一种类型安全的管道,用于在协程之间传递数据。通道可以保证数据的同步传递,避免了数据竞争和并发访问的问题。可以把通道想象成一个管道,数据通过这个管道从一个协程流向另一个协程,确保数据的有序和安全传递。可以用 ⛓️ 图标表示通道。

(二)通道的创建与使用

通道的创建使用 make 函数,有两种类型:无缓冲通道和有缓冲通道。

无缓冲通道

ch := make(chan int)

无缓冲通道在发送和接收操作时必须同时进行,否则发送或接收操作会阻塞。

有缓冲通道

ch := make(chan int, 3)

有缓冲通道可以存储一定数量的数据,发送操作在缓冲区未满时不会阻塞,接收操作在缓冲区不为空时不会阻塞。

以下是一个简单的代码示例:

package main

import (
	"fmt"
	"time"
)

func main() {
	ch := make(chan int)

	go func() {
		fmt.Println("Sending data...")
		ch <- 42 // 发送数据到通道
		fmt.Println("Data sent")
	}()

	time.Sleep(1 * time.Second)
	fmt.Println("Receiving data...")
	data := <-ch // 从通道接收数据
	fmt.Println("Received data:", data)
}

在这个示例中,一个协程向通道发送数据,而 main 协程从通道接收数据。由于通道是无缓冲的,发送操作会阻塞,直到接收操作发生。

(三)使用通道在协程间传递数据

以下是一个更复杂的示例,展示如何使用通道在多个协程间传递数据:

package main

import (
	"fmt"
	"sync"
)

func producer(ch chan<- int, wg *sync.WaitGroup) {
	defer wg.Done()
	for i := 0; i < 5; i++ {
		ch <- i
	}
	close(ch)
}

func consumer(ch <-chan int, wg *sync.WaitGroup) {
	defer wg.Done()
	for num := range ch {
		fmt.Println("Received:", num)
	}
}

func main() {
	var wg sync.WaitGroup
	ch := make(chan int)

	wg.Add(2)
	go producer(ch, &wg)
	go consumer(ch, &wg)

	wg.Wait()
	fmt.Println("All done")
}

在这个示例中:

  • producer 函数将数据发送到通道,并在发送完数据后关闭通道。
  • consumer 函数使用 for...range 从通道接收数据,当通道关闭时,for...range 会自动结束。
  • chan<- int 表示只发送通道,<-chan int 表示只接收通道,这保证了数据只能单向流动,增强了代码的安全性。

五、协程的同步与互斥 🔒

(一)互斥锁(Mutex)的使用场景

互斥锁用于保护共享资源,防止多个协程同时访问共享数据,避免数据竞争。例如,当多个协程同时访问和修改一个全局变量时,可能会导致不可预期的结果,使用互斥锁可以确保同一时间只有一个协程可以访问该变量。可以用 🔐 图标表示互斥锁。

以下是一个使用互斥锁的示例:

package main

import (
	"fmt"
	"sync"
	"time"
)

var (
	counter int
	mu      sync.Mutex
)

func increment(wg *sync.WaitGroup) {
	defer wg.Done()
	mu.Lock()
	counter++
	mu.Unlock()
}

func main() {
	var wg sync.WaitGroup
	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go increment(&wg)
	}
	wg.Wait()
	fmt.Println("Counter value:", counter)
}

在这个示例中:

  • mu.Lock() 用于锁定共享资源,mu.Unlock() 用于解锁。
  • counter 是一个全局变量,多个协程通过 increment 函数对其进行加 1 操作。
  • 互斥锁确保每次只有一个协程能修改 counter,避免了数据竞争。

(二)使用 WaitGroup 实现协程同步

我们已经在之前的示例中使用过 sync.WaitGroup,它是一种同步机制,用于等待一组协程完成任务。Add 方法增加等待组的计数,Done 方法减少计数,Wait 方法阻塞直到计数为 0。可以用 👥 图标表示等待组。

以下是另一个使用 WaitGroup 的示例,展示如何等待多个协程完成不同的任务:

package main

import (
	"fmt"
	"sync"
	"time"
)

func worker(id int, wg *sync.WaitGroup) {
	defer wg.Done()
	fmt.Printf("Worker %d starting\n", id)
	time.Sleep(time.Duration(id) * time.Second)
	fmt.Printf("Worker %d done\n", id)
}

func main() {
	var wg sync.WaitGroup
	for i := 1; i <= 5; i++ {
		wg.Add(1)
		go worker(i, &wg)
	}
	wg.Wait()
	fmt.Println("All workers done")
}

在这个示例中,每个 worker 协程会睡眠一段时间,模拟不同的任务时间,WaitGroup 确保 main 函数等待所有协程完成后才继续执行。

六、协程的生命周期管理 🌱

(一)如何优雅地结束协程

协程的生命周期通常由其函数的执行结束或程序终止而结束。但有时我们需要提前终止协程,一种方法是使用通道来发送终止信号。

以下是一个示例:

package main

import (
	"fmt"
	"time"
)

func worker(done chan bool) {
	for {
		select {
		case <-done:
			fmt.Println("Worker stopping")
			return
		default:
			fmt.Println("Worker running")
			time.Sleep(1 * time.Second)
		}
	}
}

func main() {
	done := make(chan bool)
	go worker(done)
	time.Sleep(5 * time.Second)
	done <- true
	time.Sleep(1 * time.Second)
	fmt.Println("Main done")
}

在这个示例中:

  • worker 协程使用 select 语句监听 done 通道。
  • done 通道接收到信号时,协程会退出。

(二)处理协程中的错误

在协程中处理错误非常重要,一种常见的方法是使用通道来传递错误信息。

以下是一个处理协程错误的示例:

package main

import (
	"fmt"
	"sync"
)

func worker(id int, errCh chan<- error) {
	defer func() {
		if r := recover(); r!= nil {
			errCh <- fmt.Errorf("Worker %d panicked: %v", id, r)
		}
	}()
	if id == 2 {
		panic("Something went wrong in worker 2")
	}
}

func main() {
	var wg sync.WaitGroup
	errCh := make(chan error)

	for i := 1; i <= 3; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			worker(id, errCh)
		}(i)
	}

	go func() {
		wg.Wait()
		close(errCh)
	}()

	for err := range errCh {
		if err!= nil {
			fmt.Println(err)
		}
	}
}

在这个示例中:

  • worker 函数使用 recover 来捕获 panic 并将错误发送到 errCh 通道。
  • main 函数使用 for...rangeerrCh 接收错误信息并处理。

七、协程的性能优势 💪

(一)对比传统线程模型的性能提升

传统的线程模型在创建和切换时需要操作系统的介入,开销较大。而 Go 语言的协程由 Go 运行时管理,创建和切换的开销极小。以下是一个简单的性能测试:

package main

import (
	"fmt"
	"sync"
	"time"
)

func threadTask() {
	time.Sleep(10 * time.Millisecond)
}

func goroutineTask() {
	time.Sleep(10 * time.Millisecond)
}

func main() {
	start := time.Now()
	var wg sync.WaitGroup
	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			threadTask()
		}()
	}
	wg.Wait()
	threadTime := time.Since(start)

	start = time.Now()
	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go goroutineTask()
	}
	wg.Wait()
	goroutineTime := time.Since(start)

	fmt.Printf("Thread time: %v\nGoroutine time: %v\n", threadTime, goroutineTime)
}

这个示例通过创建 1000 个线程和 1000 个协程执行相同的任务并睡眠,对比它们的执行时间,可以发现协程的性能优势。

(二)在高并发场景下的表现

在高并发场景下,如 Web 服务器或数据处理服务,协程的性能优势更加明显。由于可以创建大量的协程而无需过多的资源开销,Go 语言可以轻松处理数以万计的并发连接。例如,一个简单的 HTTP 服务器可以使用协程来处理每个请求,而不会因为大量的并发连接而导致性能下降。可以用 🌐 图标表示高并发场景。

以下是一个简单的 HTTP 服务器示例:

package main

import (
	"fmt"
	"net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hello, World!")
}

func main() {
	http.HandleFunc("/", handler)
	fmt.Println("Starting server at :8080")
	if err := http.ListenAndServe(":8080", nil); err!= nil {
		fmt.Println("Server failed:", err)
	}
}

在这个示例中,Go 的 HTTP 服务器会为每个请求创建一个协程来处理,而无需手动管理线程和连接池,充分发挥了协程的优势。

八、实际应用案例 🛠️

(一)Web 服务器中的协程应用

以下是一个更复杂的 Web 服务器示例,展示如何使用协程处理不同的请求:

package main

import (
	"fmt"
	"net/http"
	"sync"
	"time"
)

func handleRequest(w http.ResponseWriter, r *http.Request, wg *sync.WaitGroup) {
	defer wg.Done()
	fmt.Printf("Handling request from %s\n", r.RemoteAddr)
	time.Sleep(1 * time.Second)
	fmt.Fprintf(w, "Request handled by %s\n", r.RemoteAddr)
}

func main() {
	var wg sync.WaitGroup
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		wg.Add(1)
		go handleRequest(w, r, &wg)
	})

	fmt.Println("Starting server at :8080")
	if err := http.ListenAndServe(":8080", nil); err!= nil {
		fmt.Println("Server failed:", err)
	}
}

在这个示例中,handleRequest 函数会在协程中处理每个请求,使用 sync.WaitGroup 确保请求得到正确处理。

(二)数据处理任务中的协程使用

假设我们需要处理大量的数据,例如处理一个大文件中的数据行:

package main

import (
	"bufio"
	"fmt"
	"os"
	"sync"
)

func processLine(line string, wg *sync.WaitGroup, resultCh chan<- string) {
	defer wg.Done()
	// 这里可以进行数据处理,如解析、转换等操作
	resultCh <- "Processed: " + line
}

func main() {
	file, err := os.Open("large_file.txt")
	if err!= nil {
		fmt.Println("Error opening file:", err)
		return
	}
	defer file.Close()

	var wg sync.WaitGroup
	resultCh := make(chan string)

	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		wg.Add(1)
		go processLine(scanner.Text(), &wg, resultCh)
	}

	go func() {
		wg.Wait()
		close(resultCh)
	}()

	for result := range resultCh {
		fmt.Println(result)
	}
}

在这个示例中:

  • processLine 函数处理文件中的每一行数据,使用协程并发处理。
  • sync.WaitGroup 确保所有行都被处理完。
  • 处理结果通过 resultCh 通道传递和接收。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

PyCharm文档管理

背景&#xff1a;使用PyCharmgit做文档管理 需求&#xff1a;需要PyCharm自动识别docx/xslx/vsdx等文件类型&#xff0c;并在PyCharm内点击文档时唤起系统内关联应用(如word、excel、visio) 设置步骤&#xff1a; 1、file -》 settings -》file types 2、在Files opened i…

卷积神经05-GAN对抗神经网络

卷积神经05-GAN对抗神经网络 使用Python3.9CUDA11.8Pytorch实现一个CNN优化版的对抗神经网络 简单的GAN图片生成 CNN优化后的图片生成 优化模型代码对比 0-核心逻辑脉络 1&#xff09;Anacanda使用CUDAPytorch2&#xff09;使用本地MNIST进行手写图片训练3&#xff09;…

基于springboot的租房网站系统

作者&#xff1a;学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等 文末获取“源码数据库万字文档PPT”&#xff0c;支持远程部署调试、运行安装。 项目包含&#xff1a; 完整源码数据库功能演示视频万字文档PPT 项目编码&#xff1…

创建 WordPress 插件(第一部分):添加管理页面

WordPress 是互联网上最受欢迎的内容管理系统之一。它是用 PHP 创建的&#xff0c;可以处理从博客到商业网站的一切需求。事实上&#xff0c;我们的博客和网站都使用 WordPress。在本文中&#xff0c;我将向你展示如何创建一个 WordPress 插件&#xff0c;该插件会在管理员控制…

「港科技」联手「地平线」打造GPT风格的自动驾驶世界模型:DrivingWorld

摘要 最近在自回归&#xff08;AR&#xff09;生成模型方面的成功&#xff0c;例如自然语言处理中的GPT系列&#xff0c;激发了在视觉任务中复制这一成功的努力。一些研究尝试将这种方法扩展到自动驾驶中&#xff0c;通过构建基于视频的世界模型来生成逼真的未来视频序列和预测…

FPGA工程师成长四阶段

朋友&#xff0c;你有入行三年、五年、十年的职业规划吗&#xff1f;你知道你所做的岗位未来该如何成长吗&#xff1f; FPGA行业的发展近几年是蓬勃发展&#xff0c;有越来越多的人才想要或已经踏进了FPGA行业的大门。很多同学在入行FPGA之前&#xff0c;都会抱着满腹对职业发…

SOME/IP协议详解 基础解读 涵盖SOME/IP协议解析 SOME/IP通讯机制 协议特点 错误处理机制

车载以太网协议栈总共可划分为五层&#xff0c;分别为物理层&#xff0c;数据链路层&#xff0c;网络层&#xff0c;传输层&#xff0c;应用层&#xff0c;其中今天所要介绍的内容SOME/IP就是一种应用层协议。 SOME/IP协议内容按照AUTOSAR中的描述&#xff0c;我们可以更进一步…

为ARM64架构移植Ubuntu20.04换源的发现

在为ARM64架构(RK3566)移植ubuntu20.04的时候发现在更换为国内源之后&#xff0c;无法正常完成apt update,报错为: Ign:25 http://mirrors.aliyun.com/ubuntu focal-updates/main arm64 Packages …

Playwright vs Selenium:全面对比分析

在现代软件开发中&#xff0c;自动化测试工具在保证应用质量和加快开发周期方面发挥着至关重要的作用。Selenium 作为自动化测试领域的老牌工具&#xff0c;长期以来被广泛使用。而近年来&#xff0c;Playwright 作为新兴工具迅速崛起&#xff0c;吸引了众多开发者的关注。那么…

【全套】基于机器学习的印度森林火灾发生概率的分析与预测

【私信送源码文档】基于机器学习的印度森林火灾发生概率的分析与预测 对应的ppt 摘 要 随着全球气候变化的不断加剧&#xff0c;火灾的频发和规模逐渐增大&#xff0c;成为备受关注的问题。本文旨在提高对火灾发生概率的准确性&#xff0c;为火灾的预防和管理提供科学支持。在…

【Go】Go Gin框架初识(一)

1. 什么是Gin框架 Gin框架&#xff1a;是一个由 Golang 语言开发的 web 框架&#xff0c;能够极大提高开发 web 应用的效率&#xff01; 1.1 什么是web框架 web框架体系图&#xff08;前后端不分离&#xff09;如下图所示&#xff1a; 从上图中我们可以发现一个Web框架最重要…

TCP/IP协议簇及封装与解封装

TCP/IP协议簇 现如今用的参考模型TCP/IP 是一个协议簇&#xff0c;它组建了整个互联网 最主要的是TCP/IP 和这两个协议&#xff0c;所以起名为TCP/IP TCP/IP模型 TCP/IP标准模型——四层 TCP/IP对等模型——五层 数据链路层分为两个子层&#xff1a; LLC子层&#xff1a;逻辑…

《基于卷积神经网络的星图弱小目标检测》论文精读

Dim small target detection based on convolutinal neural network in star image 摘要 由于低信噪比目标和复杂背景&#xff0c;星图中弱小目标的检测是一项具有挑战性的任务。本文提出了一种深度学习方法&#xff0c;用于在背景不均匀和不同类型的噪声下检测单帧星图中的弱…

如何选择Ubuntu版本

一、为什么要选择Ubuntu系统&#xff1f; CentOS官方已全面停止维护CentOS Linux项目 。具体来说&#xff0c;CentOS 8已于2021年12月31日停止维护&#xff0c;而CentOS 7则在2024年6月30日结束了生命周期 。这意味着这些版本不再接收官方的安全更新、bug修复或技术支持 二、…

计算机视觉算法实战——视频分析(Video Analysis)

✨个人主页欢迎您的访问 ✨期待您的三连 ✨ ✨个人主页欢迎您的访问 ✨期待您的三连 ✨ ✨个人主页欢迎您的访问 ✨期待您的三连✨ ​​​​​​ ​​​​​​​​​​​​ ​​​​​ 视频分析是计算机视觉中的一个重要领域&#xff0c;旨在从视频数据中提取有用的信息&…

O2O同城系统架构与功能分析

2015工作至今&#xff0c;10年资深全栈工程师&#xff0c;CTO&#xff0c;擅长带团队、攻克各种技术难题、研发各类软件产品&#xff0c;我的代码态度&#xff1a;代码虐我千百遍&#xff0c;我待代码如初恋&#xff0c;我的工作态度&#xff1a;极致&#xff0c;责任&#xff…

讲一下ZooKeeper的持久化机制?

大家好&#xff0c;我是锋哥。今天分享关于【讲一下ZooKeeper的持久化机制&#xff1f;】面试题。希望对大家有帮助&#xff1b; 讲一下ZooKeeper的持久化机制&#xff1f; 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 ZooKeeper 是一个开源的分布式协调服务&…

C++ 文字识别OCR

一.引言 文字识别&#xff0c;也称为光学字符识别&#xff08;Optical Character Recognition, OCR&#xff09;&#xff0c;是一种将不同形式的文档&#xff08;如扫描的纸质文档、PDF文件或数字相机拍摄的图片&#xff09;中的文字转换成可编辑和可搜索的数据的技术。随着技…

数据库(MySQL)练习

数据库&#xff08;MySQL&#xff09;练习 一、练习1.15练习1.16练习 二、注意事项2.1 第四天 一、练习 1.15练习 win11安装配置MySQL超详细教程: https://baijiahao.baidu.com/s?id1786910666566008458&wfrspider&forpc 准备工作&#xff1a; mysql -uroot -p #以…

【HTML+CSS+JS+VUE】web前端教程-35-字体图标

优点: 轻量性:加载速度快,减少http请求 灵活性:可以利用CSS设置大小颜色等 兼容性:网页字体支持所有现代浏览器,包括IE低版本 使用字体图标: 1、注册账户并登录 2、选取图标或搜索图标 3、添加购物车 4、下载代码 5、选择font-class引用 iconfont Logo:https://www.ic…