GO面经总结

系列一

1. Go 的垃圾回收机制? GMP 模型?
  • Go 使用一个并发的、三色标记、写屏障的垃圾收集器。
  • GMP 是 Goroutine、M (OS 线程) 和 P (处理器) 的缩写,它描述了 Go 运行时调度的模型。其中 P 表示可执行 goroutine 队列和相关资源的逻辑处理器,M 是系统线程,G 是 goroutine。
2. 如何优雅关闭一个 channel?
  • 只有发送者应该关闭 channel,接收者只读取数据。
  • 在所有数据都发送完毕之后,通过调用 close(chan) 来关闭 channel。
3. Go 里面的 map 解决 hash 冲突的方法?
  • Go 的 map 实现使用链地址法来解决哈希冲突,即在同一个桶位置上形成一个链表存放所有键值对。
4. slice 是引用传递还是值传递?
  • Slice 在传递时是值传递,但这个值包含指向底层数组的指针,因此函数内部对 slice 的修改会影响原 slice。
5. Go 读写锁的概念?
  • sync.RWMutex 是 Go 语言的读写锁。
  • 读锁之间不互斥,读写和写写之间互斥。
  • 默认情况下,Go 的读写锁没有明确的读优先或写优先规则,执行顺序依赖于调度和锁请求的顺序。
6. context 的应用场景?
  • 用于控制子协程的生命周期(取消信号)、传递请求级别的数据、超时控制等。
7. select 的作用?
  • 用于同时等待多个通道操作,可以处理异步 IO、超时等情况。
8. 数组和切片的区别?
  • 数组是固定大小,内存分配时其长度就确定了;切片是可变长,底层引用一个数组,并提供容量和长度的动态管理。
9. map 是否是线程安全的,如何实现线程安全的 map?
  • Go 中的 map 不是线程安全的。为了线程安全,可以使用 sync.Map 或者自己在 map 上加 sync.RWMutex 锁。
10. sync.map 的原理?
  • sync.Map 使用了一个特殊的算法,它与普通的 map 相比,可以安全地被多个协程并发访问和修改,而无需额外的锁定或协调。
11. Go 数据类型有哪些?
  • 基本类型:布尔型、数值型、字符串
  • 复合类型:数组、切片、结构体、指针、函数、接口、map、channel
12. 如何判断两个 interface{} 相等?
  • 使用 == 进行比较,但需要注意的是,如果 interface{} 内部存储的是指针,则比较的是地址。
13. Go map 中删除一个 key 的内存是否会立即释放?
  • 不会立即释放,具体取决于垃圾回收器的行为。
14. init() 方法的特性?
  • init() 函数在包初始化时调用,用于初始化操作,每个包可以有多个 init() 函数。
15. switch-case 语句,强制执行下一个 case?
  • Go 的 switch-case 默认不贯穿(fallthrough),需要显式使用 fallthrough 关键字。
16. encoding/json 包解码 JSON 数据时,默认将数字解析为 float64 类型?
  • 是的,如果你使用 interface{} 接收 JSON 中的数字,它会被转换为 float64
17. Go 里面的类型断言?
  • 类型断言用于检查接口值是否保存了特定的类型,例如 value, ok := i.(int)
18. Go 静态类型声明?
  • Go 是静态类型语言,变量在编译时已经确定了类型,不能改变。
19. sync 包使用?
  • 提供基础的同步原语,如互斥锁 (sync.Mutex), 读写锁 (sync.RWMutex), WaitGroup, Once 等。
20. gin 的并发请求、错误处理、路由处理?
  • Gin 是一个高性能的 HTTP web 框架,支持并发请求处理;
  • 错误处理可以通过中间件和 c.Error() 方法实现;
  • 路由处理通过定义路由和关联的处理函数(Handlers)。
21. CSP 并发模型?
  • Communicating Sequential Processes (CSP) 是 Go 语言并发模型的基础,通过 goroutines (并行执行单元) 和 channels (通信机制) 实现。
22. 逃逸分析的介绍?
  • 逃逸分析是编译器用于决定变量分配位置(栈或堆)的过程。在 Go 中,使用 -gcflags "-m" 查看编译器关于逃逸分析的信息。
23. 对关闭的 channel 进行读写会发生什么?
  • 对关闭的 channel 读取会返回零值;
  • 对关闭的 channel 写入会导致 panic。
24. 字符串转 byte 数组会发生内存拷贝么?
  • 是的,将 string 转换为 []byte 会发生内存拷贝。
25. 如何实现字符串转切片无内存拷贝(unsafe)?
  • 使用 unsafe 包可以避免内存拷贝,但这种做法可能不安全,需要谨慎处理。
26. Go 语言 channel 的特性? channel 阻塞情况? channel 底层实现?
  • Channel 是 Go 中的通信机制,可以在协程之间传递数据。
  • 如果 channel 满了,发送者会阻塞;如果空了,接收者会阻塞。
  • Channel 的底层实现基于队列结构,涉及到一些同步原语。

系列二

哪些数据结构是线程不安全的

在许多编程语言中,基本的数据结构通常是线程不安全的,因为它们没有内置的同步机制。在 Go 语言中,以下是线程不安全的数据结构:

  • Slices
  • Maps
  • Built-in types (int, float, bool, etc.)

Map为什么是线程不安全的

Go 中的 map 是线程不安全的,因为当多个 goroutines 同时读写一个 map(尤其是有一个进行写操作)时,可能会发生并发冲突,导致运行时异常如“fatal error: concurrent map writes”。

Channel阻塞可以实现什么场景(计数,令牌桶)

Channel 阻塞可以用于多种场景,例如:

  • 计数信号量:通过有缓冲的 channel 实现计数,限制同时运行的 goroutines 数量。
  • 令牌桶算法:用于流量控制,通过 channel 来限制事件的发生速率。

Mysql什么时候是行锁什么时候是表锁

MySQL 会根据使用的存储引擎和查询类型来决定使用行锁或表锁。例如:

  • MyISAM 默认使用表锁。
  • InnoDB 支持行锁和表锁,默认情况下使用行锁。

MySQL有几种错误读(脏读、幻读等等)

在事务中,错误的读取类型主要有三种:

  • 脏读:读取到另一个未提交事务的数据。
  • 不可重复读:在同一事务中多次读取同样记录的结果不一致。
  • 幻读:在同一事务中,一个范围查询的结果改变了,因为另一个事务插入或删除了符合该范围的行。

MySQL默认事务隔离级别是什么

MySQL 默认的事务隔离级别是 REPEATABLE READ

假如有个sql联了多个表还有子查询,怎么优化

优化方法可能包括:

  • 确保所有被连接的字段都有索引。
  • 分析查询执行计划(EXPLAIN)以寻找性能瓶颈。
  • 重新设计查询,使其更高效,比如将子查询替换为 JOIN 操作。
  • 使用临时表或物化视图来存储中间结果。

你平常是怎么优化MySQL的

优化 MySQL 的常见方法:

  • 创建合适的索引,并定期检查它们的有效性。
  • 优化查询语句,避免全表扫描。
  • 调整 MySQL 配置,如缓存大小和缓冲池。
  • 使用分区表提高大表的管理效率和查询性能。
  • 定期维护数据库,进行碎片整理。

Kafka为什么快?

Kafka 之所以快,主要得益于:

  • 顺序 I/O:Kafka 把消息顺序写入磁盘,这种方式比随机 I/O 快得多。
  • 零拷贝技术:直接从文件系统缓存传输数据到网络缓冲区,减少数据复制。
  • 分区和多副本:提供并行处理能力和负载均衡。
  • 批处理:在网络和磁盘 I/O 中进行消息批处理,降低系统调用的开销。

Kafka怎么实现消息不丢失

Kafka 可以通过以下方式来确保消息不丢失:

  • 在生产者端设置 acks 参数为 all,确保所有副本都收到消息后才认为消息已提交。
  • 使用幂等生产者和/或事务确保消息不重复。
  • 在消费者端正确处理偏移量,确保消息被成功处理后再提交偏移量。

Kafka是顺序写还是随机写

Kafka 是顺序写的,这是它快速高效的关键原因之一。

Go协程是怎么扩展内存的(找P要)

Go 协程开始时有一个很小的栈(通常是2KB),当需要更多空间时,栈会自动增长。当栈不够用时,Go 运行时会检测到这一点,并分配一个更大的栈空间,然后把原来的数据复制过去。这个过程称为栈扩展。

讲一下你对Docker和K8s的理解

  • Docker:是一个开源的应用容器引擎,让开发者可以打包他们的应用及依赖到一个可移植的容器中,然后发布到任何支持 Docker 的 Linux 机器上,也可以实现虚拟化。
  • Kubernetes (K8s):是一个强大的容器编排工具,用于自动化部署、扩展和管理容器化应用程序。提供服务发现、负载均衡、自我修复、自动滚动更新等功能。

说一下某种集群的leader选举策略(举例了redis)

Redis Sentinel 系统用于管理多个 Redis 服务器,包括故障转移和leader选举。Sentinel 通过发送心跳包进行监控,并在 master 宕机时自动进行leader选举,选择新的 master。

Redis中什么是主观下线,什么是客观下线。

  • 主观下线:当 Sentinel 判定一个 Redis 实例无法访问时,它会对该实例进行主观下线。
  • 客观下线:当多个 Sentinel 都报告同一个实例不可达时,那么该实例会被客观下线,并触发故障转移。

聊一聊你对GRPC的理解。

gRPC 是一个高性能、开源和通用的 RPC 框架,由 Google 主导开发。它支持多种语言,在 HTTP/2 上实现请求/响应、流式传输,利用 Protocol Buffers 作为接口描述语言。

为什么gRPC传输比JSON快

gRPC 通常比 JSON 快,因为:

  • 它使用 Protocol Buffers,这是一种轻量、高效的二进制格式。
  • 它基于 HTTP/2,支持多路复用、服务器推送等高效特性。

大量定时任务需要在凌晨1点都准时开始执行

要处理大量定时任务,有几种方法可以保证它们能够准时并且高效地执行:

  1. 使用分布式任务调度框架

    • 例如 Apache Airflow, Celery (对于 Python),或者 Go 的分布式调度器如 dkron。
    • 这些框架会负责触发任务,并且可以扩展到多个服务器以分散负载。
  2. 负载均衡与批处理

    • 如果所有任务必须同时开始,可以将它们分成小批次,每个批次被不同的工作服务器处理。
    • 可以采用消息队列(如 Kafka、RabbitMQ)来管理这些批次的调度。
  3. 资源分配与限流

    • 确保系统有足够的资源来同时执行这些任务,或者基于当前系统负载动态调整可执行的任务数量。
    • 对数据库和其他共享资源进行访问限制,避免因为一下子过多的请求导致崩溃。
  4. 预热

    • 在执行之前预热应用实例,确保代码已经加载到内存中,连接池已建立等。
  5. 监控与报警

    • 实时监控任务执行状态,出现问题及时报警并自动恢复。
  6. 冗余设计

    • 确保任务调度系统的高可用性,比如通过设置主备调度器。
  7. 时间同步

    • 确保所有参与任务调度的服务器时间完全同步,可以使用 NTP 服务保持时间一致。

消息发送过多导致大量堆积怎么处理

面临消息堆积问题时,可以从以下几个方向寻找解决办法:

  1. 增加消费者

    • 增加更多的消费者进程或者线程来处理消息队列中的消息,从而提升处理速度。
  2. 优化处理逻辑

    • 分析并优化消息的处理逻辑,提高单个消费者的处理速度。
  3. 水平扩展

    • 增加更多的服务器以扩展系统的消费能力。
  4. 优先级队列

    • 如果有些消息比其他消息更紧急,可以考虑实现优先级队列,确保重要的消息首先被处理。
  5. 消息压缩

    • 如果消息内容太大,考虑对消息进行压缩。
  6. 消息削峰

    • 在消息发送方实施限流策略,比如令牌桶或漏桶算法,以平滑发送速率。
  7. 监控与报警

    • 对消息队列进行实时监控,一旦出现堆积,立即通知相关人员进行处理。
  8. 临时存储

    • 如果是暂时的高峰期造成的堆积,可以考虑将消息写入快速的临时存储,如 Redis,稍后再慢慢处理。
  9. 消息持久化

    • 确保所有未处理的消息都被持久化存储,防止系统崩溃导致数据丢失。
  10. 稳定性优先

    • 如果系统无法处理过多的消息,可以退回部分消息或拒绝接收新消息,保证系统稳定运行。

处理消息堆积的关键在于监控系统状况,并根据实际情况调整资源,以及优化处理逻辑来应对。此外,系统设计时也应考虑到可能出现的高峰情况,做好相应的扩展和容错设计。

有缓冲channel和无缓冲channel的区别

在 Go 语言中,channel 是协程间通信的一种机制,它们可以是有缓冲的(buffered)或无缓冲的(unbuffered)。

  • 无缓冲channel:当你向无缓冲channel发送数据时,发送者会阻塞直到另一个 goroutine 读取了这些数据。同样,如果从无缓冲channel读取,读取者也会阻塞直到有数据被写入。因此,无缓冲channel提供了一种同步交换数据的方式。

  • 有缓冲channel:有缓冲的channel具有固定的容量,只有在缓冲区满时,尝试向其发送数据的操作才会阻塞。同理,只有在缓冲区为空时,尝试从中读取数据的操作才会阻塞。有缓冲channel允许发送者和接收者在缓冲区不满和不空的情况下异步工作。

选择使用有缓冲还是无缓冲channel取决于你的具体需求,比如是否需要强同步或者希望减少因等待锁而产生的延迟。

了解gin的中间件吗,讲一下你对他的了解

Gin 是一个用 Go (golang) 编写的 HTTP web 框架。它内置了一组丰富的中间件功能,使得用户可以轻松地为请求添加处理逻辑。

中间件是 Gin 路由过程中的处理函数,它可以:

  • 在处理请求之前执行某些操作(例如身份验证、日志记录、限流)。
  • 修改请求和响应。
  • 终止请求链并直接返回响应。

Gin 的中间件支持全局注册和单个路由注册,也允许创建自定义中间件。中间件的执行顺序按照它们被添加到路由器上的顺序。

select 满足多个case的时候怎么执行的

select 语句中存在多个 case 同时满足时,Go 运行时会随机选择一个执行。这保证了每个可运行的 case 都有相同的机会被选中,避免了总是倾向于优先选择特定 case 而导致的偏差。

如果有一个全局变量怎么保证并发安全

要保证全局变量的并发安全,可以采用以下方法:

  • 使用互斥锁(Mutex)来保护对全局变量的访问。
  • 使用原子操作,如 sync/atomic 包中的函数,来进行简单的变量更新操作。
  • 将全局变量封装进一个协程,并通过 channel 与该协程通信来安全地读写全局变量。

CPU高问题如何解决?

解决CPU使用率高的问题通常包括以下步骤:

  1. 分析:首先需要确定是什么导致了CPU使用率高。这通常涉及性能监控和分析工具,比如 pprof 来识别高CPU消耗的代码部分。
  2. 优化:根据分析结果,可以通过算法优化、减少不必要的计算、并发执行等方法来降低CPU使用率。
  3. 扩展:如果应用程序因为负载增加而导致CPU使用率高,可能需要水平扩展(增加更多服务器)或垂直扩展(升级服务器配置)。
  4. 代码审查:检查代码中是否存在无效循环或密集型操作,以及是否所有并发执行都经过适当同步。

知道哀设计模式?

设计模式是软件设计中普遍存在、反复出现问题的解决方案。在面向对象的设计中,常见的设计模式包括但不限于:

  • 创建型模式:如 Singleton(单例)、Factory Method(工厂方法)、Abstract Factory(抽象工厂)、Builder(建造者)、Prototype(原型)。
  • 结构型模式:如 Adapter(适配器)、Composite(组合)、Proxy(代理)、Flyweight(享元)、Facade(外观)、Bridge(桥接)、Decorator(装饰者)。
  • 行为型模式:如 Strategy(策略)、Observer(观察者)、Command(命令)、State(状态)、Visitor(访问者)、Mediator(中介者)、Iterator(迭代器)、Chain of Responsibility(责任链)、Memento(备忘录)。

这些设计模式有助于编写易于维护和扩展的代码,同时也便于团队之间的沟通。

Linux 查看磁盘占用

在 Linux 中,有几个命令可以帮助你检查磁盘使用情况:

  1. df (disk free): 显示文件系统的总空间、已用空间、可用空间和挂载点。

    df -h
    

    -h 参数让输出以人类可读的格式显示(例如使用 MB、GB 等单位)。

  2. du (disk usage): 报告目录或文件的磁盘使用空间。

    du -sh /path/to/directory
    

    -s 参数表示汇总每个参数的大小;-h 使其以易读格式显示。

  3. ncdu (NCurses Disk Usage): 是 du 的一种图形化版本,它提供了一个交互式界面来浏览系统,并查找占用大量空间的文件。

    ncdu /path/to/directory
    

    如果系统中没有安装 ncdu,可以使用包管理器安装,如 sudo apt-get install ncdu

  4. ls: 列出文件和目录,结合 -l-h 参数可以显示文件的大小。

    ls -lh
    
  5. iotop or atop: 实时监控磁盘 I/O 和磁盘占用。

  6. baobab (Disk Usage Analyzer): GNOME 桌面环境下的磁盘使用分析工具,提供了一个图形化界面。

  7. find: 查找超过特定大小的文件。

    find / -type f -size +100M
    

    这将会列出系统上所有超过 100MB 的文件。

通过这些工具,你可以有效地管理和监控 Linux 系统的磁盘空间,确保关键任务的顺利运行。

GC(垃圾回收)的过程描述

不同的编程语言和环境有不同的垃圾回收机制。以下是大多数现代垃圾回收算法通用的高层次步骤:

  1. 标记阶段:

    • 垃圾回收器遍历所有从根对象(通常是全局变量和活跃线程的执行栈上的变量)可达的对象。
    • 遍历期间,所有可达的对象被标记为活动的,这意味着它们当前正在使用中,不能被回收。
  2. 清除/删除非活动对象:

    • 将未标记的对象认为是垃圾,并进行回收。这些对象占用的内存将被释放并返回到内存池。
    • 在某些回收算法中,比如标记-清除(Mark-Sweep),此步骤直接将未标记的内存释放。
    • 在其他算法中,如标记-整理(Mark-Compact)或复制(Copying),会移动活动对象以消除内存碎片,然后回收剩余的全部内存区域。
  3. 压缩阶段 (可选):

    • 特定的回收器可能会包括一步用于压缩内存的阶段,即整理存活的对象,使它们在内存中连续排列,减小碎片化,优化后续分配的性能。

在实际应用程序中,垃圾回收可能更复杂,因为它涉及到如何识别垃圾、如何处理并发执行、如何最小化停顿时间等问题。

例如,在 Java 虚拟机(JVM)中,垃圾回收器可以采用几种不同的策略,如串行回收、并行回收、CMS(Concurrent Mark-Sweep)、G1(Garbage-First)等,每种都有其优缺点和适用场景。

在 Go 语言中,垃圾回收器是并发执行的,并且设计成尽量减少程序运行的暂停时间。Go 的GC算法主要基于三色标记法,并且在近年的迭代中一直在改进以减少STW(stop-the-world)事件的频率和持续时间。

Channel 的使用场景

在 Go 语言中,channel 是用于协程之间通信的强大工具。以下是一些常见的 channel 使用场景:

  1. 消息传递:在 goroutines 之间安全地传递数据。
  2. 同步操作:使用无缓冲 channel 实现多个 goroutine 之间的同步。
  3. 信号广播:通过关闭 channel 来广播一个信号,告诉其他 goroutine 一个事件已发生。
  4. 任务队列:使用有缓冲 channel 创建一个工作队列,并由多个 worker 并发处理。
  5. 限制并发数:用有缓冲 channel 控制同时运行的 goroutines 数量,以避免过载。
  6. 数据流管道:将一系列处理阶段连接起来,每个阶段由一个或多个 goroutines 执行,并且它们之间使用 channel 传递数据(类似 Unix 管道)。

channel关闭之后再读和再关闭会发生什么?

  • 关闭之后再读:关闭一个 channel 后,已经发送到 channel 中的数据仍然可以被接收,直到 channel 中的数据都被读取完。当所有数据被读取后,任何对该 channel 的进一步读取都将立即返回而不会阻塞,并且返回的值为该类型的零值。这意味着你可以安全地从一个已关闭的 channel 中读取数据,但需要考虑如何正确处理零值。

  • 关闭之后再关闭:一个 channel 只能被关闭一次。再次尝试关闭同一个 channel 将会导致 panic。因此,在关闭 channel 之前应确保只有一个 goroutine 负责关闭,并且没有其他 goroutine 会尝试重复关闭它。

map中的数据delete之后内存会回收吗?

在 Go 中,当你使用 delete() 函数从 map 中删除键值对时,该键值对所占用的内存确实可能被垃圾回收器回收。但这不是立即发生的,回收的时间依赖于垃圾回收器的调度。

delete() 函数只是将键值对从 map 中移除,使其不再可访问。垃圾收集器稍后将检查不再被任何引用的内存块,并回收它

Kafka的消息丢失和消息重复消费:

  • 消息丢失可能发生在生产者发送失败、Kafka集群故障或消费者未能成功提交offset导致的重启后重新消费而跳过某些消息。
  • 消息重复消费可能由网络问题导致生产者重试发送、消费者处理完消息但提交offset失败,重启后重新消费相同消息。

Kafka和RabbitMQ的区别:

  • 架构:Kafka基于发布订阅模型,设计为高吞吐量分布式系统;RabbitMQ是传统的消息队列,更强调消息的可靠性和多种消息模式。
  • 推拉模式:Kafka主要是消费者拉取(polling)消息,而RabbitMQ支持推送(push)消息给消费者。

拉的模式有什么好处:

  • 控制消费速度:消费者可以根据自己的处理能力来拉取消息,避免被动接收导致的处理瓶颈。

使用分布式锁的过程中应用挂了:

  • 优雅启停+defer:确保释放锁的操作在defer语句中执行,即使应用异常终止也能保证资源的释放。
  • 使用过期时间+自动续期:设置锁的过期时间并定期续期,如果应用挂掉,锁将因超时而自动释放。

对象存储和文件存储的主要区别:

  • 对象存储提供通过API访问非结构化数据,无文件层次结构,每个对象包含数据、元数据和全局唯一标识。适合大数据和云存储。
  • 文件存储则是基于文件系统提供文件或文件夹的存取,有层次结构,适合传统应用。

分片上传实现:

  • 分片上传通过将大文件分成小块独立上传,最后再合并这些块。
  • 文件合并后进行hash一致性校验,以确认数据完整性。
  • 秒传可以通过比对已存在的文件hash值来实现,如果文件已存在,则不需要重新上传,直接创建索引即可。

邮箱验证码功能实现:

  • 使用Redis存储验证码及其过期时间,结合邮箱服务组件发送验证码到用户邮箱。

JWT的格式:

  • JWT由三部分组成:Header(头部,包含加密算法)、Payload(负载,包含内容)、Signature(签名,用于验证)。

defer的原理:

  • defer语句将其后面跟随的函数推迟到当前函数返回之前执行。

map的底层结构:

  • Go语言的map底层是一个哈希表,使用数组和链表组合实现。

map中hash冲突解决:

  • 使用链表存储具有相同哈希值的不同键值对。
  • 当链表过长时,转换为平衡树(如红黑树)以提高查找效率。

Go性能调优案例:

  • 使用pprof工具分析CPU和内存使用情况,通过分析线程日志找出性能瓶颈。

看线程在日志里的状态:

  • 线程日志通常会标记线程状态,展示是否在循环或等待事件。

MySQL的事务隔离级别:

  • Read Uncommitted
  • Read Committed
  • Repeatable Read
  • Serializable

可重复读:

  • 指在同一个事务中,多次读取同一数据结果都是一致的,即使其他事务正在修改该数据。

事务实现的底层原理:

  • 事务通过锁机制、MVCC(多版本并发控制)等手段来实现ACID特性。

Redis持久化机制:

  • RDB:定期快照存储数据状态。
  • AOF:记录所有写操作命令并在重启时重放。

持久化时为什么是fork子进程:

  • 避免阻塞主进程,提高数据安全性,利用操作系统的写时复制(Copy-On-Write)技术减少内存使用。

Docker基本原理:

  • Docker使用Linux容器技术,通过cgroups和namespace实现资源隔离和限制,UnionFS为容器提供轻量级的、可写的文件系统。

其他容器运行时:

  • 例如Podman, containerd, rkt等。

K8s的组件:

  • 节点组件:kubelet, kube-proxy, Container Runtime
  • 控制面组件:kube-apiserver, kube-controller-manager, kube-scheduler, etcd

构建deployment的control:

  • Kubernetes中的Deployment对象通常是通过kubectl命令行工具或者使用YAML配置文件定义的,然后由kube-apiserver处理并存储到etcd中,由控制器管理器中的deployment控制器进行实际的构建和维护。
  • 58
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值