Go语言学习(四)

一、rune类型

rune 是 Go 语言特有的数据类型,专门用于处理 Unicode 字符,尤其是多字节字符(如中文、日文、韩文等)。理解 rune 对于正确处理国际化文本至关重要。

在go中,rune本质是int32类型。它的作用是表示一个 Unicode 码点(code point),范围从 00x10FFFF,可以表示世界上几乎所有的字符。

(1)为什么需要rune类型

Go 中的 string 是字节的集合,每个 byte 占 1 个字节(8 位),只能表示 ASCII 字符(0-127)。而 Unicode 字符(如中文)通常需要多个字节表示(UTF-8 编码下占 3 个字节),因此直接用 byte 处理会导致错误。

直接用 byte 处理中文的问题

Go 复制代码
package main

import "fmt"

func main() {
    s := "你好,世界"
    fmt.Println("字符串长度(字节数):", len(s)) // 输出:15(每个中文占3字节,标点也占3字节)
    
    // 尝试用索引访问字符(错误方式)
    for i := 0; i < len(s); i++ {
        fmt.Printf("%c ", s[i]) // 输出乱码:ä ½  å ¥½ ¼ Œ  ä ¸– ç � Œ 
    }
}

len(s) 返回的是字符串的字节数,而非字符数;直接通过索引访问得到的是单个字节,而非完整字符。

(2)将字符串转换为 rune 切片

通过 []rune(s) 可以将字符串转换为 rune 切片,每个元素对应一个 Unicode 字符:

Go 复制代码
package main

import "fmt"

func main() {
    s := "你好,世界"
    runes := []rune(s) // 转换为 rune 切片
    
    fmt.Println("字符数:", len(runes)) // 输出:5(正确的字符数量)
    
    // 遍历 rune 切片(正确访问每个字符)
    for i, r := range runes {
        fmt.Printf("索引 %d: %c(Unicode码点:%U)\n", i, r, r)
    }
}

字符数: 5
索引 0: 你(Unicode码点:U+4F60)
索引 1: 好(Unicode码点:U+597D)
索引 2: ,(Unicode码点:U+FF0C)
索引 3: 世(Unicode码点:U+4E16)
索引 4: 界(Unicode码点:U+754C)
(3)for range 循环遍历字符串

for range 循环会自动将字符串按 rune 解析,每次迭代返回一个 Unicode 字符:

Go 复制代码
package main

import "fmt"

func main() {
    s := "Go语言❤️"
    // for range 自动处理 rune
    for i, r := range s {
        fmt.Printf("位置 %d: 字符 %c(占 %d 字节)\n", i, r, len(string(r)))
    }
}
位置 0: 字符 G(占 1 字节)
位置 1: 字符 o(占 1 字节)
位置 2: 字符 语(占 3 字节)
位置 5: 字符 言(占 3 字节)
位置 8: 字符 ❤(占 3 字节)
位置 11: 字符 ️(占 3 字节)
(4)处理特殊字符(如 emoji、组合字符)
Go 复制代码
package main

import "fmt"

func main() {
    // 笑脸 emoji(占4字节)
    smile := '😊'
    fmt.Printf("笑脸:%c,类型:%T,Unicode码点:%U,字节数:%d\n", 
        smile, smile, smile, len(string(smile)))
    
    // 组合字符(e + 重音符号 → é)
    eAcute := 'é'
    fmt.Printf("组合字符:%c,Unicode码点:%U\n", eAcute, eAcute)
}
笑脸:😊,类型:int32,Unicode码点:U+1F60A,字节数:4
组合字符:é,Unicode码点:U+00E9
(5)字符串截取(避免截断多字节字符)

直接通过字节索引截取字符串可能截断多字节字符,使用 rune 可以安全截取

Go 复制代码
package main

import "fmt"

// 安全截取字符串前n个字符
func safeSubstring(s string, n int) string {
    runes := []rune(s)
    if n > len(runes) {
        n = len(runes)
    }
    return string(runes[:n])
}

func main() {
    s := "Hello,世界!"
    
    // 错误方式:按字节截取(可能截断)
    badSub := s[:7]
    fmt.Println("错误截取:", badSub) // 输出:Hello,(乱码)
    
    // 正确方式:按 rune 截取
    goodSub := safeSubstring(s, 7)
    fmt.Println("正确截取:", goodSub) // 输出:Hello,世
}

二、Go 语言中结构体 Tag 解析

在 Go 语言中,结构体标签(Struct Tag)是一种附着在结构体字段上的元数据,常用于 JSON 序列化、数据库映射等场景。而解析 Tag 的核心技术是 反射(Reflection),它允许程序在运行时检查和操作变量的类型与值。

(1)结构体 Tag 基础

结构体 Tag 是字段声明后用反引号 ````` 包裹的字符串,格式通常为 key:"value" key2:"value2"

Go 复制代码
package main

import "fmt"

// 定义带 Tag 的结构体
type User struct {
    Name string `json:"name" db:"user_name"`
    Age  int    `json:"age,omitempty" db:"user_age"`
}

func main() {
    u := User{Name: "Alice", Age: 30}
    fmt.Printf("%+v\n", u) // 输出:{Name:Alice Age:30}
}

Tag 本身不影响结构体的字段值,仅作为元数据存在,一个字段可以有多个 Tag(用空格分隔不同 key),常见 Tag 用途:json(序列化字段名)、db(数据库列名)、xml(XML 节点名)等。

(2)Tag 依靠反射解析

反射的核心是两个类型:

  • reflect.Type:表示变量的类型信息(静态类型)
  • reflect.Value:表示变量的值信息(动态值)

通过 reflect.TypeOf()reflect.ValueOf() 可获取任意变量的反射对象

Go 复制代码
package main

import "reflect"

func main() {
    u := User{Name: "Bob", Age: 25}
    
    // 获取类型信息(reflect.Type)
    t := reflect.TypeOf(u)
    fmt.Println("类型:", t.Name()) // 输出:User
    
    // 获取值信息(reflect.Value)
    v := reflect.ValueOf(u)
    fmt.Println("值:", v.Field(0).String()) // 输出:Bob
}

解析结构体字段的 Tag 需通过 reflect.TypeField() 方法获取字段信息,再通过 Tag.Get(key) 提取指定 key 的值

Go 复制代码
package main

import (
    "fmt"
    "reflect"
)

type User struct {
    Name string `json:"name" db:"user_name"`
    Age  int    `json:"age,omitempty" db:"user_age"`
}

func parseTags(s interface{}) {
    // 1. 获取反射类型对象(必须传入指针,否则无法修改值,但解析Tag无需修改)
    t := reflect.TypeOf(s)
    
    // 2. 检查是否为结构体(若传入指针,需先获取元素类型)
    if t.Kind() == reflect.Ptr {
        t = t.Elem() // 解引用指针,获取结构体类型
    }
    if t.Kind() != reflect.Struct {
        fmt.Println("输入不是结构体或结构体指针")
        return
    }
    
    // 3. 遍历结构体字段
    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i) // 获取第i个字段的类型信息
        fmt.Printf("字段名:%s\n", field.Name)
        
        // 4. 解析Tag
        jsonTag := field.Tag.Get("json")   // 获取json标签
        dbTag := field.Tag.Get("db")       // 获取db标签
        fmt.Printf("  json标签:%s\n", jsonTag)
        fmt.Printf("  db标签:%s\n", dbTag)
    }
}

func main() {
    u := User{}
    parseTags(&u) // 传入指针(推荐方式)
    // 输出:
    // 字段名:Name
    //   json标签:name
    //   db标签:user_name
    // 字段名:Age
    //   json标签:age,omitempty
    //   db标签:user_age
}
  • reflect.Type.Field(i):获取结构体第 i 个字段的 StructField 对象(包含字段名、类型、Tag 等信息)
  • StructField.Tag:字段的 Tag 信息(类型为 reflect.StructTag
  • StructTag.Get(key):提取 Tag 中指定 key 的值(自动忽略空格和引号)

reflect.StructTag.Get(key) 的底层逻辑是解析 Tag 字符串,按规则提取值。简化的实现思路如下

Go 复制代码
func getTag(tag string, key string) string {
    // Tag 格式示例:`json:"name" db:"user_name"`
    // 1. 按空格分割多个 key-value 对
    // 2. 对每个部分解析为 key:"value" 格式
    // 3. 匹配 key 并返回对应的 value(去除引号)
    // 实际实现需处理各种边缘情况(如引号内的空格、转义字符等)
    return "" // 实际逻辑见 Go 源码:src/reflect/type.go
}

三、反射的核心原理

反射的本质是程序在运行时对自身结构的检查能力,Go 语言的反射系统建立在类型系统之上,核心原理包括

(1)类型信息的存储

Go 语言中,每个变量在编译时都会被赋予一个类型信息,存储在可执行文件中。运行时,通过 reflect.TypeOf 可以获取这个类型信息的副本(reflect.Type)。

  • 静态类型:变量声明时的类型(如 var a intint 是静态类型)
  • 动态类型:接口变量实际指向的值的类型(仅接口有动态类型)
(2)反射的三大法则
  • 从接口值到反射对象
Go 复制代码
var x int = 10
t := reflect.TypeOf(x)  // t 是 int 类型的反射对象
v := reflect.ValueOf(x) // v 是 10 这个值的反射对象
  • 从反射对象到接口值
Go 复制代码
v := reflect.ValueOf(10)
x := v.Interface().(int) // 类型断言,恢复为 int
  • 要修改反射对象,其值必须可设置

反射对象可设置的前提:它持有原始变量的地址(即通过指针获取的反射对象)

Go 复制代码
var x int = 10
v := reflect.ValueOf(x)
fmt.Println(v.CanSet()) // 输出:false(v 是副本,不可修改)

vPtr := reflect.ValueOf(&x).Elem() // 解引用指针,获取原始值的反射对象
fmt.Println(vPtr.CanSet()) // 输出:true(可修改)
vPtr.SetInt(20)
fmt.Println(x) // 输出:20(原始变量被修改)

四、goroutine什么情况下会阻塞

goroutine 是轻量级线程,由 Go 运行时(而非操作系统)调度。虽然 goroutine 设计初衷是高效并发,但在多种场景下会进入阻塞状态 (暂停执行,等待特定条件满足后恢复)。以下是 goroutine 可能阻塞的所有常见情况。

(1)通道操作导致的阻塞

通道是 goroutine 间通信的主要方式,未正确处理的通道操作是阻塞的最常见原因。

1、向无缓冲通道发送数据(ch <- data

阻塞条件 :无缓冲通道(make(chan T))发送数据时,必须有对应的接收操作(<-ch),否则发送方 goroutine 会阻塞,直到有 goroutine 接收数据

Go 复制代码
package main

import "fmt"

func main() {
    ch := make(chan int) // 无缓冲通道

    // 启动发送 goroutine
    go func() {
        fmt.Println("发送前")
        ch <- 10 // 阻塞:等待接收方
        fmt.Println("发送后") // 接收方处理后才执行
    }()

    // 延迟接收(模拟处理耗时)
    // 若注释掉下面一行,发送方会永久阻塞(导致死锁)
    fmt.Println("接收值:", <-ch)
}

发送前
接收值:10
发送后

2、从无缓冲通道接收数据(data := <-ch

阻塞条件 :无缓冲通道接收数据时,若通道中没有数据,接收方 goroutine 会阻塞,直到有 goroutine 发送数据。

Go 复制代码
package main

import "fmt"

func main() {
    ch := make(chan int) // 无缓冲通道

    // 启动接收 goroutine
    go func() {
        fmt.Println("接收前")
        val := <-ch // 阻塞:等待发送方
        fmt.Println("接收值:", val)
    }()

    // 延迟发送(模拟处理耗时)
    ch <- 20 // 发送数据,唤醒接收方
}

接收前
接收值:20

3、向缓冲通道发送数据(缓冲区已满)

阻塞条件 :缓冲通道(make(chan T, n))的缓冲区已满时,发送操作会阻塞,直到有 goroutine 从通道接收数据(腾出缓冲区空间)。

Go 复制代码
package main

import "fmt"
import "time"

func main() {
    ch := make(chan int, 2) // 缓冲区大小为 2

    // 填充缓冲区(不阻塞)
    ch <- 1
    ch <- 2

    // 启动发送 goroutine(缓冲区已满,会阻塞)
    go func() {
        fmt.Println("尝试发送第3个值")
        ch <- 3 // 阻塞:缓冲区已满
        fmt.Println("第3个值发送成功") // 有接收后才执行
    }()

    // 接收一个值(腾出缓冲区空间)
    time.Sleep(3 * time.Second)
    fmt.Println("接收值:", <-ch) // 接收 1
    time.Sleep(3 * time.Second)
}

尝试发送第3个值
接收值: 1
第3个值发送成功

4、从缓冲通道接收数据(缓冲区为空)

阻塞条件 :缓冲通道的缓冲区为空时,接收操作会阻塞,直到有 goroutine 向通道发送数据。

Go 复制代码
package main

import "fmt"
import "time"

func main() {
    ch := make(chan int, 1) // 缓冲通道,初始为空

    // 启动接收 goroutine(缓冲区为空,阻塞)
    go func() {
        fmt.Println("等待接收")
        val := <-ch // 阻塞:缓冲区为空
        fmt.Println("接收到:", val)
    }()
    time.Sleep(1 * time.Second)
    // 发送数据(唤醒接收方)
    ch <- 30
    time.Sleep(1 * time.Second)
}
等待接收
接收到: 30
(2)同步原语导致的阻塞

Go 标准库 sync 包提供的同步工具(如锁、等待组)会导致 goroutine 阻塞,直到同步条件满足。

1、互斥锁(sync.Mutex)与读写锁(sync.RWMutex

  • Mutex.Lock() 阻塞 :当锁已被其他 goroutine 持有,调用 Lock()goroutine 会阻塞,直到锁被释放(Unlock())。
  • RWMutex.Lock() 阻塞:写锁被持有或有读锁时,后续写锁请求会阻塞。
  • RWMutex.RLock() 阻塞:写锁被持有,后续读锁请求会阻塞。
Go 复制代码
package main

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

var mu sync.Mutex

func main() {
    // 第一个 goroutine 持有锁
    go func() {
        mu.Lock()
        defer mu.Unlock()
        fmt.Println("goroutine1: 获得锁,开始处理")
        time.Sleep(2 * time.Second) // 模拟耗时操作
        fmt.Println("goroutine1: 释放锁")
    }()

    // 第二个 goroutine 尝试获取锁(会阻塞)
    go func() {
        fmt.Println("goroutine2: 尝试获取锁")
        mu.Lock() // 阻塞:锁被 goroutine1 持有
        defer mu.Unlock()
        fmt.Println("goroutine2: 获得锁,开始处理")
    }()

    time.Sleep(3 * time.Second) // 等待所有 goroutine 执行
}

输出顺序不一定完全和我这个一样:
goroutine1: 获得锁,开始处理
goroutine2: 尝试获取锁
goroutine1: 释放锁
goroutine2: 获得锁,开始处理

2、等待组(sync.WaitGroup

WaitGroup.Wait() 阻塞 :调用 Wait()goroutine 会阻塞,直到等待组的计数器减为 0(所有 Add(n)goroutine 都调用了 Done())。

Go 复制代码
package main

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

func main() {
    var wg sync.WaitGroup
    wg.Add(2) // 等待 2 个 goroutine

    // 启动第一个任务
    go func() {
        defer wg.Done()
        time.Sleep(1 * time.Second)
        fmt.Println("任务1完成")
    }()

    // 启动第二个任务
    go func() {
        defer wg.Done()
        time.Sleep(2 * time.Second)
        fmt.Println("任务2完成")
    }()

    fmt.Println("等待所有任务完成...")
    wg.Wait() // 阻塞:等待计数器为 0
    fmt.Println("所有任务完成")
}
等待所有任务完成...
任务一完成
任务二完成
所有任务完成

3、条件变量(sync.Cond

Cond.Wait() 阻塞 :调用 Wait()goroutine 会释放关联的锁并阻塞,直到被 Signal()Broadcast() 唤醒(唤醒后需重新获取锁)。

Go 复制代码
package main

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

func main() {
    mu := &sync.Mutex{}
    cond := sync.NewCond(mu)
    ready := false

    // 等待者 goroutine
    go func() {
        cond.L.Lock()
        defer cond.L.Unlock()

        // 循环等待条件满足(避免虚假唤醒)
        for !ready {
            fmt.Println("等待者:条件不满足,阻塞")
            cond.Wait() // 释放锁并阻塞
        }
        fmt.Println("等待者:条件满足,继续执行")
    }()

    // 通知者 goroutine
    go func() {
        time.Sleep(1 * time.Second) // 模拟准备时间
        cond.L.Lock()
        ready = true // 满足条件
        cond.L.Unlock()
        fmt.Println("通知者:发送唤醒信号")
        cond.Signal() // 唤醒一个等待的 goroutine
    }()

    time.Sleep(2 * time.Second)
}
等待者:条件不满足,阻塞
通知者:发送唤醒信号
等待者:条件满足,继续执行

cond.Wait() 内部做了三件事:

a. 释放已持有的锁 (cond.L.Unlock())。

b. 阻塞当前 goroutine,等待通知。

c. 当被唤醒时,重新获取锁 (cond.L.Lock())。

cond.Signal() 唤醒的是谁?

cond.Signal() 会唤醒正在等待(即正在执行 cond.Wait())的 goroutine 中的任意一个,这些 goroutine 都绑定在同一个 cond 变量上。它唤醒的不是"拥有这个cond的goroutine",而是"等待这个cond的goroutine"。

拥有者 :条件变量本身没有"拥有者"的概念。锁 (mutex) 有"持有者",但条件变量没有。

等待队列 :每个 sync.Cond 实例内部维护了一个等待队列 ,所有在这个 cond 上调用 Wait() 的 goroutine 都会被加入到这个队列中。

cond.Signal() 的工作流程是:

  1. 从与该 cond 关联的等待队列中,取出第一个(Go 运行时不保证是严格FIFO,但可以理解为最先等待的或任意一个)goroutine。

  2. 将这个 goroutine 标记为可唤醒状态。

下面给出有多个等待者的情况:

Go 复制代码
package main

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

func main() {
	mu := &sync.Mutex{}
	cond := sync.NewCond(mu)
	ready := false

	// 创建 3 个等待者 goroutine
	for i := 1; i <= 3; i++ {
		id := i
		go func() {
			cond.L.Lock()
			defer cond.L.Unlock()

			for !ready {
				fmt.Printf("等待者 %d:条件不满足,阻塞\n", id)
				cond.Wait()
				fmt.Printf("等待者 %d:被唤醒,检查条件\n", id)
			}
			fmt.Printf("等待者 %d:条件满足,继续执行\n", id)
		}()
	}

	// 通知者 goroutine
	go func() {
		time.Sleep(1 * time.Second)
		cond.L.Lock()
		ready = true
		cond.L.Unlock()
		fmt.Println("通知者:发送唤醒信号 (Signal)")
		cond.Signal() // 只唤醒一个等待者!
	}()

	time.Sleep(3 * time.Second)
	fmt.Println("\n--- 使用 Broadcast 的例子 ---")

	// 重置,演示 Broadcast
	mu2 := &sync.Mutex{}
	cond2 := sync.NewCond(mu2)
	ready2 := false

	for i := 1; i <= 3; i++ {
		id := i
		go func() {
			cond2.L.Lock()
			defer cond2.L.Unlock()

			for !ready2 {
				fmt.Printf("等待者 %d:条件不满足,阻塞\n", id)
				cond2.Wait()
				fmt.Printf("等待者 %d:被唤醒,检查条件\n", id)
			}
			fmt.Printf("等待者 %d:条件满足,继续执行\n", id)
		}()
	}

	time.Sleep(500 * time.Millisecond)
	go func() {
		time.Sleep(1 * time.Second)
		cond2.L.Lock()
		ready2 = true
		cond2.L.Unlock()
		fmt.Println("通知者:发送广播信号 (Broadcast)")
		cond2.Broadcast() // 唤醒所有等待者!
	}()

	time.Sleep(3 * time.Second)
}

等待者 1:条件不满足,阻塞
等待者 2:条件不满足,阻塞
等待者 3:条件不满足,阻塞
通知者:发送唤醒信号 (Signal)
等待者 1:被唤醒,检查条件
等待者 1:条件满足,继续执行

--- 使用 Broadcast 的例子 ---
等待者 3:条件不满足,阻塞
等待者 2:条件不满足,阻塞
等待者 1:条件不满足,阻塞
通知者:发送广播信号 (Broadcast)
等待者 1:被唤醒,检查条件
等待者 1:条件满足,继续执行
等待者 3:被唤醒,检查条件
等待者 3:条件满足,继续执行
等待者 2:被唤醒,检查条件
等待者 2:条件满足,继续执行

当使用 cond.Signal() 时,只有一个等待者会被唤醒并继续执行。

当使用 cond.Broadcast() 时,所有三个等待者都会被唤醒并继续执行。

(3)网络与 I/O 操作导致的阻塞

goroutine 执行网络请求、文件读写等 I/O 操作时,会进入阻塞状态,直到 I/O 完成。

1、网络请求(HTTP、TCP 等)

Go 复制代码
package main

import (
    "fmt"
    "net/http"
)

func main() {
    // 启动 goroutine 发起网络请求(会阻塞)
    go func() {
        fmt.Println("开始请求网页...")
        resp, err := http.Get("https://www.baidu.com") // 阻塞:等待响应
        if err == nil {
            fmt.Printf("请求完成,状态码:%d\n", resp.StatusCode)
            resp.Body.Close()
        }
    }()

    // 等待请求完成
    var input string
    fmt.Scanln(&input) // 阻塞主线程,避免程序退出
}

开始请求网页...
请求完成,状态码:200

2、文件读写

阻塞条件 :读写文件时,goroutine 会阻塞,直到操作完成(数据读取 / 写入完毕)。

Go 复制代码
package main

import (
    "fmt"
    "os"
)

func main() {
    // 启动 goroutine 读取文件(会阻塞)
    go func() {
        fmt.Println("开始读取文件...")
        data, err := os.ReadFile("test.txt") // 阻塞:等待文件读取完成
        if err == nil {
            fmt.Printf("文件读取完成,长度:%d 字节\n", len(data))
        } else {
            fmt.Println("读取错误:", err)
        }
    }()

    // 等待操作完成
    var input string
    fmt.Scanln(&input)
}
(4)其他阻塞场景

1、time.Sleep()

阻塞条件goroutine 会暂停执行指定的时间,期间处于阻塞状态。

Go 复制代码
package main

import (
    "fmt"
    "time"
)

func main() {
    go func() {
        fmt.Println("开始休眠...")
        time.Sleep(2 * time.Second) // 阻塞 2 秒
        fmt.Println("休眠结束")
    }()

    time.Sleep(3 * time.Second) // 等待子 goroutine 执行
}

2、等待操作系统信号(os.Signal

Go 复制代码
package main

import (
    "fmt"
    "os"
    "os/signal"
    "syscall"
    "time"
)

func main() {
    // 创建信号通道
    sigChan := make(chan os.Signal, 1)
    signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) // 监听 Ctrl+C 等信号

    // 启动 goroutine 等待信号(阻塞)
    go func() {
        fmt.Println("等待信号(按 Ctrl+C 发送 SIGINT)...")
        sig := <-sigChan // 阻塞:等待信号
        fmt.Printf("收到信号:%v,退出程序\n", sig)
        os.Exit(0)
    }()

    // 主线程无限循环
    for {
        time.Sleep(1 * time.Second)
    }
}

// 等待信号(按 Ctrl+C 发送 SIGINT)...
// ^C收到信号:interrupt,退出程序
相关推荐
猷咪7 分钟前
C++基础
开发语言·c++
IT·小灰灰8 分钟前
30行PHP,利用硅基流动API,网页客服瞬间上线
开发语言·人工智能·aigc·php
快点好好学习吧10 分钟前
phpize 依赖 php-config 获取 PHP 信息的庖丁解牛
android·开发语言·php
秦老师Q11 分钟前
php入门教程(超详细,一篇就够了!!!)
开发语言·mysql·php·db
烟锁池塘柳011 分钟前
解决Google Scholar “We‘re sorry... but your computer or network may be sending automated queries.”的问题
开发语言
是誰萆微了承諾11 分钟前
php 对接deepseek
android·开发语言·php
2601_9498683615 分钟前
Flutter for OpenHarmony 电子合同签署App实战 - 已签合同实现
java·开发语言·flutter
星火开发设计29 分钟前
类型别名 typedef:让复杂类型更简洁
开发语言·c++·学习·算法·函数·知识
qq_1777673741 分钟前
React Native鸿蒙跨平台数据使用监控应用技术,通过setInterval每5秒更新一次数据使用情况和套餐使用情况,模拟了真实应用中的数据监控场景
开发语言·前端·javascript·react native·react.js·ecmascript·harmonyos
一匹电信狗42 分钟前
【LeetCode_21】合并两个有序链表
c语言·开发语言·数据结构·c++·算法·leetcode·stl