开发中go语言的作用之详细分析

开发中go语言的作用详细讲解

后续还会持续更新ing 感谢大家的收藏和点赞
有错请指出

结构体

结构体概念

Go 语言中数组可以存储同一类型的数据,但在结构体中我们可以为不同项定义不同的数据类型。

结构体是由一系列具有相同类型或不同类型的数据构成的数据集合。

结构体定义需要使用 type 和 struct 语句。struct 语句定义一个新的数据类型,结构体中有一个或多个成员。type 语句设定了结构体的名称。结构体的格式如下:

type struct_variable_type struct {
   member definition
   member definition
   ...
   member definition
}

定义结构体
比如在菜鸟教程中
package main

import "fmt"

type Books struct {
   title string
   author string
   subject string
   book_id int
}


func main() {

    // 创建一个新的结构体
    fmt.Println(Books{"Go 语言", "www.runoob.com", "Go 语言教程", 6495407})

    // 也可以使用 key => value 格式
    fmt.Println(Books{title: "Go 语言", author: "www.runoob.com", subject: "Go 语言教程", book_id: 6495407})

    // 忽略的字段为 0 或 空
   fmt.Println(Books{title: "Go 语言", author: "www.runoob.com"})
}

访问结构体成员
可以把结构体理解为一个对象,然后访问成员就是访问对象的属性

比如

package main

import "fmt"

type people struct {
	age  int
	name string
}

func main() {
	ljl := people{19, "ljl"}

	fmt.Println(ljl.name, ljl.age)//打印ljl和19

}

结构体在开发中的作用

1.表示实体

我们可以写一个结构体来储存用户的信息,便于查询

type User struct {
    ID       int
    Username string
    Email    string
    Password string 
}

2.配置信息
type Config struct {
    Addr     string
    Port     int
    Timeout  time.Duration
}
3.数据交换

例如我们客户端发的请求需要和服务端交换数据

我们有一个提供用户信息的 API 服务。客户端可以通过发送 HTTP 请求来获取用户数据,服务器则会返回 JSON 格式的用户信息。

  1. 定义用户结构体
    首先,我们定义一个用户结构体,它将用于在服务器和客户端之间传输数据。
type User struct {
    ID       int    `json:"id"`
    Username string `json:"username"`
    Email    string `json:"email"`
    // 其他字段...
}
  1. 序列化数据
    服务器端在处理用户数据时,需要将用户结构体序列化为 JSON 格式,以便通过网络发送给客户端。
func getUserInfo(userID int) (User, error) {
    // 假设我们有一些逻辑来获取用户信息,例如从数据库中查询
    // ...
    user := User{
        ID:       userID,
        Username: "exampleUser",
        Email:    "user@example.com",
    }
    return user, nil
}

func serveUserInfo(w http.ResponseWriter, r *http.Request) {
    // 解析请求参数,获取用户ID
    vars := mux.Vars(r)
    userID, err := strconv.Atoi(vars["id"])
    if err != nil {
        http.Error(w, "Invalid user ID", http.StatusBadRequest)
        return
    }

    // 获取用户信息
    user, err := getUserInfo(userID)
    if err != nil {
        http.Error(w, "User not found", http.StatusNotFound)
        return
    }

    // 序列化用户信息为JSON
    jsonData, err := json.Marshal(user)
    if err != nil {
        http.Error(w, "Internal Server Error", http.StatusInternalServerError)
        return
    }

    // 设置响应的Content-Type并写入JSON数据
    w.Header().Set("Content-Type", "application/json")
    w.Write(jsonData)
}
  1. 反序列化数据
    客户端接收到 JSON 数据后,需要将其反序列化为相应的结构体,以便在应用程序中使用。
func fetchUserInfo(userID int) (User, error) {
    // 构建请求URL
    url := fmt.Sprintf("http://api.example.com/users/%d", userID)

    // 发送HTTP请求获取数据
    resp, err := http.Get(url)
    if err != nil {
        return User{}, err
    }
    defer resp.Body.Close()

    // 检查响应状态码
    if resp.StatusCode != http.StatusOK {
        return User{}, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
    }

    // 反序列化JSON数据
    var user User
    if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
        return User{}, err
    }

    return user, nil
}

解释一下err
比如我拿这端代码举例子

resp, err := http.Get(url)
    if err != nil {
        return User{}, err
    }

err你拼也这道是错误的意思,因为我们的程序难免会出错,err就是这个错误的信息,如果我们获取url出错,那么err值不为nil,这个nil表示的就是一个检查的指标,那么就会打印错误的信息
全是个人理解,希望发现错误纠正

然后我们再从上面理解一下结构体的作用,在上面结构体的重点就是储存信息,然后传递信息,转换信息,感觉把结构体理解为一个类就好理解了

4.函数和方法参数

我们先看例子,再分析作用
函数参数

type Point struct {
    X int
    Y int
}

// Move 函数接受一个 Point 类型的参数,并返回一个新的 Point
func Move(p Point, dx int, dy int) Point {
    p.X += dx
    p.Y += dy
    return p
}

func main() {
    p := Point{X: 1, Y: 2}
    p = Move(p, 3, 4)
    fmt.Println(p) // 输出:{4, 6}
}

我们可以看到结构体作为参数传入我们的函数,如果我们不使用结构体,我们还需要一个一个传入参数,这也体现了结构体的封装的作用

当函数需要处理多个相关联的参数时,使用结构体可以将这些参数封装在一起,提高代码的可读性和可维护性。

方法参数

type Circle struct {
    Radius float64
}

// Area 方法计算并返回圆的面积
func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}

func main() {
    c := Circle{Radius: 5}
    area := c.Area()
    fmt.Println(area) // 输出:78.53981633974483
}

发现这个例子中,我们给结构体定义了一个方法,用来计算圆的面积,这样我的优势就是把对象和方法结合,方便我对一个对象进行操作

当需要对某个对象进行一系列操作时,使用方法可以将这些操作与对象类型关联起来,使得代码更加直观和易于理解。

数据存储和序列化

我们知道我们数据储存不是说你写什么,我就储存什么,我们一直提到的序列化就是来解决这个问题的,能够把我们是数据转化成其他形式储存

结构体可以方便地与JSON、XML或其他数据格式进行序列化和反序列化,这在处理数据存储和API交互时非常有用。

至于为什么这样说

还是举例子说明吧

type User struct {
    ID      int    `json:"id"`//这个``就是tags,为结构体提供额外的信息,这个表示信息可以被json处理
    Name    string `json:"name"`
    Email   string `json:"email"`
}

func main() {
    // 创建一个用户实例
    user := User{
        ID:    1,
        Name:  "John Doe",
        Email: "john@example.com",
    }

    // 将用户数据序列化为 JSON 字符串
    jsonData, err := json.Marshal(user)
    if err != nil {
        log.Fatal(err)
    }

    // 输出 JSON 数据
    fmt.Println(string(jsonData)) // 输出:{"id":1,"name":"John Doe","email":"john@example.com"}
}

映射关系:结构体User的字段与JSON对象的属性之间建立了直接的映射关系。每个结构体字段都对应JSON中的一个键,这使得序列化过程非常直观。

类型安全:结构体User定义了每个字段的类型(int、string),这保证了在序列化和反序列化过程中数据的类型是正确的。

标签(Tags):结构体字段后面的反引号中的内容是标签(tags),例如json:“id”。这些标签指示了在序列化成JSON时,结构体的字段应该如何被命名。在这个例子中,结构体字段的名称与JSON键的名称相同,但是如果你想要在JSON中使用不同的名称,可以通过标签来指定。

标准库支持:json.Marshal函数是Go标准库encoding/json的一部分,它知道如何处理结构体和它们的标签。这使得将结构体转换为JSON变得非常简单,只需要一行代码。

易用性:整个序列化过程非常简单,只需创建一个结构体实例,并调用json.Marshal函数。不需要编写复杂的转换逻辑或手动处理每个字段。

代码的可维护性:如果以后需要添加、删除或修改User结构体的字段,你只需要在一个地方(结构体定义)进行修改。所有的序列化和反序列化代码都会自动适应这些变化,因为它们都是基于结构体的定义。

错误处理:json.Marshal函数返回一个错误值,这允许你处理可能发生的任何错误,例如,如果结构体中包含不能被序列化的字段类型。

再简单说一下反序列化吧

type User struct {
    ID      int    `json:"id"`
    Name    string `json:"name"`
    Email   string `json:"email"`
}

func main() {
    // 假设我们有以下 JSON 数据
    jsonData := []byte(`{"id":1,"name":"John Doe","email":"john@example.com"}`)

    // 创建一个 User 变量来存储反序列化的数据
    var user User

    // 将 JSON 数据反序列化为 User 结构体实例
    err := json.Unmarshal(jsonData, &user)
    if err != nil {
        log.Fatal(err)
    }

    // 输出用户信息
    fmt.Printf("User: %+v\n", user) // 输出:User: {ID:1 Name:John Doe Email:john@example.com}
}

我们只需要创建一个实体去接受,不需要更多的操作

嵌入和组合

组合

type Address struct {
    Street string
    City   string
    State  string
    Zip    string
}

type Employee struct {
    Name    string
    Role    string
    Address Address // 组合 Address 类型
}

我们看到一个结构体作为一个属性到宁外一个结构体里面去了,这样减少代码复杂程度

这种方式允许 Employee 类型使用 Address 类型的所有字段,但 Address 的方法不会被 Employee 继承。

嵌入

type Base struct {
    Name string
}

func (b Base) SayHello() {
    fmt.Println("Hello, my name is", b.Name)
}

type Derived struct {
    Base   // 嵌入 Base 类型
    Age    int
}

func main() {
    d := Derived{
        Base: Base{Name: "John"},
        Age:  30,
    }

    d.SayHello() // 输出:Hello, my name is John
}

可以看到,我们的Derived里面嵌入了Base,仔细看区别,如果是嵌入,那是直接作为我的一部分,所以这意味着 Derived 类型不仅包含了 Base 类型的所有字段,还继承了 Base 类型的所有方法。所以d实例可以直接调用Sayhello方法

组合的使用场景
当你需要将多个类型的特性组合成一个更复杂的类型时。
当你想要明确地表示一个类型包含另一个类型的实例时。
嵌入的使用场景
当你需要继承一个类型的字段和方法时。
当你想要重用代码并且让嵌入的类型像外部类型的一部分一样工作时。

切片

什么是切片就不说了,这个不难
主要讲讲它在开发中的作用

动态数据

听名字都知道优势在哪里,在现实世界里,我们很多东西都不是固定的,比如一个典型,用户读取数据的时候,你作为提供数据的,不知道用户会读取多少,这时候就需要动态的处理方式

lines := []string{}
for {
    line, err := reader.ReadString('\n')
    if err == io.EOF {
        break
    }
    lines = append(lines, line)
}

用户读一行,我在读取数据添加一行就好了

数据处理

比如对数据进行过滤,添加映射关系,归并等处理
下面举个例子慢慢体会

切片提供了丰富的操作,如切片、追加、复制和删除元素。这些操作使得切片在处理数据时非常灵活。

package main

import "fmt"

type Product struct {
    Name     string
    Price    float64
    IsAvailable bool
}

func filterProducts(products []Product, condition func(Product) bool) []Product {
    result := []Product{}
    for _, p := range products {
        if condition(p) {
            result = append(result, p)
        }
    }
    return result
}

func mapPrices(products []Product) []float64 {
    prices := []float64{}
    for _, p := range products {
        prices = append(prices, p.Price)
    }
    return prices
}

func totalCost(prices []float64) float64 {
    total := 0.0
    for _, price := range prices {
        total += price
    }
    return total
}

func main() {
    products := []Product{
        {"Apple", 1.0, true},
        {"Banana", 0.5, false},
        {"Cherry", 2.0, true},
    }

    // Filter available products
    availableProducts := filterProducts(products, func(p Product) bool { return p.IsAvailable })
    fmt.Println(availableProducts) // Output: [{Apple 1 true} {Cherry 2 true}]

    // Map to prices
    prices := mapPrices(availableProducts)
    fmt.Println(prices) // Output: [1 2]

    // Total cost
    total := totalCost(prices)
    fmt.Println(total) // Output: 3
}

然后上面涉及一些知识,比如随便一个方法

func filterProducts(products []Product, condition func(Product) bool) []Product {
    result := []Product{}
    for _, p := range products {
        if condition(p) {
            result = append(result, p)
        }
    }
    return result
}

函数里面参数代表什么意义
products []Product

products是参数名,[]Product表示参数类型。这种写法表示products是一个切片(slice),它包含的元素类型是Product。

func(p Product) bool
这个写法是闭包函数,可以在定义的地方立即使用,可以作为参数传递

func:这是关键字,用于声明一个函数。

(p Product):这是函数的参数列表。在这个例子中,函数有一个参数p,其类型为Product。这个参数是函数体内用来引用传递给它的Product类型值的变量。

bool:这是函数的返回类型。这个匿名函数返回一个布尔值,表示传入的Product是否满足条件。

for _, p := range products
表示遍历切片的每一个元素
for:这是关键字,用于开始一个循环。

range:这是关键字,用于指定循环的迭代方式。在这里,它告诉Go遍历products切片中的每个元素。

products:这是要遍历的切片。

_:这是一个特殊的变量名,用来表示我们不关心迭代的索引。在Go中,range循环默认返回两个值:索引和元素值。如果我们不需要索引,可以使用_来忽略它。

p:这是在每次迭代中接收当前元素的变量名。在这个例子中,p代表当前的Product对象。

然后再看看切片起的作用,它首先对数据进行了一个过滤,遍历元素,然后选符合的元素,也就是我们的condition返回为ture的元素

映射,看第二个函数
把每个p对应了price,生成了一个prices的数组

归并,第三个函数

利用我们映射生成的prices,进行切片取出每一个price然后相加

多维数据结构

我们的数据很多不是一个数组能够解决的,什么二维三维还是很多的
什么很难理解
这样,你想象一下坐标轴,有x的x,y的 有x,y,z的
线,面,体吗

下面举个例子
比如一个地图
我表示一个对象在地图的哪里就需要x,y这种坐标

type GameObject struct {
    Name string
    Type string
    // 其他属性,如生命值,攻击力等
}
type GameMap [][]*GameObject  // 我们使用指向游戏对象的指针,这样我们可以在地图上共享对象

// 创建一个新的地图
gameMap := make(GameMap, height)  // 首先,我们创建一个“行”切片
for i := range gameMap {
    gameMap[i] = make([]*GameObject, width)  // 然后,我们为每一行创建一个“列”切片
}
player := &GameObject{"Player 1", "player"}

// 将玩家放置在地图上的某个位置
gameMap[playerY][playerX] = player

上面就体现了多维数据结构的作用

作为参数传递
func main() {
    numbers := []int{1, 2, 3, 4, 5}
    sum := calculateSum(numbers)
    fmt.Println(sum) // 输出:15
}

func calculateSum(numbers []int) int {
    total := 0
    for _, number := range numbers {
        total += number
    }
    return total
}

我们可以看到把切片作为参数能方便的传递一个大数据集,如果不用切片,我们还得一个一个的传入

指针

基础知识

在程序中,任何的内存都有一个地址,指针就是这个地址。我们只需要记住两个符号:&(取地址)和*(根据地址取值。)

指针类型和指针地址

GO语言中的值类型(int,float,bool,string,array,struct)都有对应的指针类型,如:*int、*string等

我们举一个简单的例子

package main

import (
	"fmt"
)

func main() {
	a := 10
	b := "nn0nkey"
	c := 1.11
	//%p用于格式化输出地址,而%T用于输出值的类型
	fmt.Printf("a:%p ptr:%T\n", &a, &a) //a:0xc00000a0c8 ptr:*int
	fmt.Printf("c:%p ptr:%T\n", &b, &b) //c:0xc000024070 ptr:*string
	fmt.Printf("d:%p ptr:%T\n", &c, &c) //d:0xc00000a0e0 ptr:*float64
}

指针取值

在对普通变量使用&操作符取地址后会获得这个变量的指针,然后可以对指针使用*操作,也就是指针取值
简单举个例子

package main

import (
	"fmt"
)

func main() {
	a := 10
	d := &a
	fmt.Printf("a:%d address:%p\n value:%d\n", a, &a, *d) //a:10 address:0xc000096068  value:10
}

可以看见*d 获取了地址对应的值

取地址操作符&和取值操作符*是一对互补操作符,&取出地址,*根据地址取出地址指向的值。

通过指针修改值
package main

import "fmt"

func example1(x int) {
	x = 1
}
func example2(x *int) { //表示传入一个地址类型的参数
	*x = 2
}
func main() {
	a := 0
	example1(a)
	fmt.Println(a) //0
	example2(&a)
	fmt.Println(a) //2

}

可以发现我们第一个方法并没有把a的值修改掉,而是二才修改完成
因为我们输出的是这个函数作用域的a值,即使方法1修改了它,但是并没有在main作用域修改,而如果我们使用指针修改,就是修改全局作用域的,所有修改成功

new和make

他们都是申请内存的
如果不申请内存的话会发送什么呢?

package main

import "fmt"

func main() {
	var a *int
	*a = 123
	fmt.Println(*a)

}

panic: runtime error: invalid memory address or nil pointer dereference
会返回panic错误

new
new函数不太常用,使用new函数得到的是一个类型的指针,并且该指针对应的值为该类型的零值

func newDemo() {
	var a *int
	a = new(int)
	*a = 123
	fmt.Println(a) //  0
	fmt.Println(*a)//123
}

可以发现我们的第一次返回的是0,因为new之后得到的还是一个指针,所有我们需要*才能获取值
make
make只用于slice、map以及channel 返回的还是这三个引用类型本身

package main

import "fmt"

func main() {
	var b map[string]int
	b = make(map[string]int)
	b["ljl"] = 123456
	fmt.Println(b)//打印123456
}

可以看到我们根本就不需要再*去给b赋值,直接打印就完事,这就是我们引用类型本身

在开发中的作用

修改函数外部的变量

函数参数是传值的,即函数接收的是参数的副本,而非参数本身。如果你希望函数能修改外部的变量,就需要使用指针。

因为作用域的关系,所以一般函数内部都不能修改函数外部的值
指针在这时候就能发挥作用

    type User struct {
        Name string
        Email string
        Age  int
    }

    func updateProfile(user *User, newName string, newEmail string, newAge int) {
        user.Name = newName
        user.Email = newEmail
        user.Age = newAge
    }

    func main() {
        user := &User{"Alice", "alice@example.com", 25}
        updateProfile(user, "Alice", "alice_new@example.com", 26)
        fmt.Println(user) // Output: &{Alice alice_new@example.com 26}
    }
优化性能

当我们只需要修改大数据集中的一个值,如果直接传入大数据集,就会非常占用内纯
这时候我们就可以使用指针,因为指针只针对一个内存地址

func processBigData(data *[]int) {
    // 这里我们可以通过解引用指针来访问和修改原始切片
    (*data)[0] = 100
}

func main() {
    bigData := make([]int, 1e6)  // 创建一个包含一百万个元素的切片
    processBigData(&bigData)  // 使用指针传递切片
    fmt.Println(bigData[0])  // 输出:100
}

可以看到我们传递参数的时候,并没有传入整个切片,而是传入的地址的引用

通过解引用这个指针,它可以访问和修改原始的bigData切片,而且不需要复制整个切片。这样可以大大提高处理大数据时的性能。

作为方法接收者

其实这和上面第一个修改函数外部的值有点像,第一个是作为参数传递,我这也是作为参数传递,但是更重要 的是作为方法的接收者
方法的接收者可以是这个类型的值,也可以是这个类型的指针

如果你希望方法能够修改接收者的状态,你需要使用指针作为接收者。

type Character struct {
    HP int
}

func (c *Character) TakeDamage(damage int) {
    c.HP -= damage
}

func main() {
    character := &Character{HP: 100}
    character.TakeDamage(20)
    fmt.Println(character.HP)  // 输出: 80
}

修改函数外部的变量的主要场景是:你已经在一个函数(比如main函数)里定义了一个变量,然后你希望另一个函数能够修改这个变量的值。在这个场景中,你会把这个变量的指针作为参数传递给另一个函数,然后在那个函数里通过这个指针来修改这个变量的值。这种情况下,修改变量的值的操作发生在函数内部。

方法接收者是指在定义方法(method)时,你可以选择使用值接收者或者指针接收者。如果你希望方法可以修改其接收者的值,那么你需要使用指针接收者。这种情况下,修改变量的值的操作发生在方法内部。

这两者的主要区别在于在哪里修改变量的值:是在函数内部,还是在方法内部。

面向编程

匿名字段

什么是匿名字段

Go语言中的结构体(struct)支持匿名字段,这是一种特殊的字段,它允许你直接从另一个结构体中继承字段。匿名字段的概念类似于其他语言中的组合(Composition)或继承(Inheritance)。

匿名字段的使用方式很简单,你只需要在结构体的字段列表中直接写另一个结构体的类型,就像这样:

type Point struct {
    X int
    Y int
}

type Circle struct {
    Point
    Radius int
}

在这个例子中,Circle 结构体包含了 Point 结构体的字段 X 和 Y,以及它自己的字段 Radius。
上面的Point就是匿名字段,这里的匿名字段主要是继承作用,去继承Point的属性

其实这也是匿名字段最主要的作用
在开发种任然是这样,方便数据的封装
和管理

小小举个例子

type Person struct {
    Name string
    Age  int
}

type User struct {
    // 匿名字段,存储个人信息
    Person

    // 其他用户特有的字段
    Username string
    Email   string
}

User 结构体包含了一个 Person 类型的字段,它存储了用户的个人信息。这有助于将用户的基本信息和用户特有的信息(如用户名和邮箱)分开存储。

接口

什么是接口

你类比一下其他语言的接口,就是没有实体,只有空的结构
当然go语言也一样
比如一个简单的接口

type Shape interface {
    Area() float64
    Perimeter() float64
}

只包括一个结构体,方法名称,参数类型

但是还有一个有意思的一点就是接口实现不需要说要声明
比如

type Rectangle struct {
    Width  float64
    Height float64
}

func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

func (r Rectangle) Perimeter() float64 {
    return 2 * (r.Width + r.Height)
}

这个类就实现了接口,虽然没有直接声明,但是它提供了Area方法
这样就代表Rectangle 类型实现了 Shape

提供统一的 API 减少代码复制程度

还是和以前一样,比如java的接口有什么作用,那这个接口也差不多

举个最容易理解的例子

type Animal interface {
    Speak() string
}

type Dog struct {
    Name string
}

type Cat struct {
    Name string
}

func (d Dog) Speak() string {
    return d.Name + " says woof!"
}

func (c Cat) Speak() string {
    return c.Name + " says meow!"
}

func main() {
    var animals []Animal
    animals = append(animals, Dog{"Rex"})
    animals = append(animals, Cat{"Whiskers"})

    for _, animal := range animals {
        fmt.Println(animal.Speak())
    }
}

我们的dog和cat都实现了animal的接口
都使用了它的speak方法,思考如果我们没有接口,那我们就要单独写,这样代码就复杂,而且这样还方便管理我的代码,因为我的speak方法都来自animal

defer关键字

defer就是Golang延迟调用

defer特性:
1. 关键字 defer 用于注册延迟调用。
2. 这些调用直到 return 前才被执。因此,可以用来做资源清理。
3. 多个defer语句,按先进后出的方式执行。
4. defer语句中的变量,在defer声明时就决定了。

资源清理

比如打开文件,如果不关闭就一直会占用内存,所以我们需要执行关闭操作,但是我们关闭操作是需要在我们对文件操作之后才关闭,这时候就可以用上defer关键字

func readFile(filename string) ([]byte, error) {
    file, err := os.Open(filename)
    if err != nil {
        return nil, err
    }
    defer file.Close()
    // 读取文件内容
    return []byte{}, nil
}

我们能够在每次return返回前关闭资源的消耗,确保我们的file进程关闭了

还比如一种情况,那就是抛出异常,这时候我们就要把进程关闭

func performOperation() {
    // 执行操作
    defer func() {
        if err := recover(); err != nil {
            fmt.Println("Error occurred:", err)
            // 释放资源
        }
    }()
}

日志记录和性能监控

我们可以使用defer在函数执行完成之后,返回它的执行时间和结果

func processData(data []byte) {
    startTime := time.Now()
    // 处理数据
    defer func() {
        fmt.Printf("Processing took %v\n", time.Since(startTime))
    }()
}

当然这也是一种性能的监控,因为我可以看到每次执行完成后,执行的时间,如果太长,那就是性能下降

  • 64
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值