go理论知识记录(入门2)

go理论知识记录(入门2)

来源于刘丹冰老师的课程

type关键字

1. 定义类型别名

使用 type 关键字可以为现有类型定义别名。

语法:

go 复制代码
type 新类型名 原类型

示例:

go 复制代码
type myInt int
  • myIntint 类型的别名,但它是一个全新的类型,不能直接与 int 类型混用。

2. 使用自定义类型

定义别名后,可以像使用原类型一样使用新类型。

示例:

go 复制代码
var a myInt = 10
fmt.Printf("%d\n", a) // 输出: 10

3. 定义结构体

结构体是一种复合数据类型,用于将多个字段组合在一起。

语法:

go 复制代码
type 结构体名 struct {
    字段1 类型1
    字段2 类型2
    ...
}

示例:

go 复制代码
type book struct {
    title string
    auth  string
}

4. 使用结构体

定义结构体后,可以创建结构体变量并访问其字段。

示例:

go 复制代码
var book1 book
book1.title = "Golang"
book1.auth = "zhangsan"

fmt.Printf("%v\n", book1) // 输出: {Golang zhangsan}

5. 完整示例代码

以下是完整的示例代码:

go 复制代码
package main

import "fmt"

// 给 int 类型起别名
type myInt int

// 声明一个结构体类型
type book struct {
    title string
    auth  string
}

func main() {
    // 使用自定义类型
    var a myInt = 10
    fmt.Printf("%d\n", a) // 输出: 10

    // 使用结构体
    var book1 book
    book1.title = "Golang"
    book1.auth = "zhangsan"

    fmt.Printf("%v\n", book1) // 输出: {Golang zhangsan}
}

6. 输出结果

运行上述代码后,输出如下:

10
{Golang zhangsan}

7. 注意事项
  1. 类型别名与原始类型

    • 类型别名是一个全新的类型,不能直接与原始类型混用。
    • 如果需要转换,必须显式进行类型转换。

    示例:

    go 复制代码
    var a myInt = 10
    var b int = int(a) // 显式类型转换
  2. 结构体字段访问

    • 结构体的字段通过 . 操作符访问。
    • 如果结构体字段未初始化,会使用其类型的零值。
  3. 值接收者 vs 指针接收者

    • 值接收者(func (this Hero))会创建对象的副本,修改不会影响原始对象。
    • 指针接收者(func (this *Hero))直接操作原始对象,修改会生效。

结构体的注意点

1. 公有与私有
  • Go 语言通过首字母大小写控制访问权限。
  • 大写字母开头的字段或方法可以在包外访问。
  • 小写字母开头的字段或方法只能在包内访问。
2. 指针接收者的使用
  • 如果需要在方法中修改结构体的字段,必须使用指针接收者。
  • 否则,修改只会作用于方法的副本,不会影响原始对象。
3. 方法命名规范
  • 方法名应清晰表达其功能。
  • 遵循 Go 语言的命名规范,使用驼峰式命名法。

好的!以下是关于 Go语言中的继承 的学习笔记片段,重点围绕你提供的代码,解释 Go 语言如何通过 结构体嵌入 实现类似继承的功能。


继承

Go 语言本身并没有继承,但通过 结构体嵌入方法提升,可以实现类似继承的效果。


1. 结构体嵌入

在代码中,Hero 结构体嵌入了 Human 结构体:

go 复制代码
type Hero struct {
    Human // 嵌入 Human 结构体
    level int
}
  • 嵌入 意味着 Hero 结构体继承了 Human 的所有字段和方法。
  • Hero 可以直接访问 Human 的字段和方法,就像访问自己的字段和方法一样。

2. 方法提升

嵌入的结构体的方法会被 自动提升 到外层结构体中。例如:

go 复制代码
func (this *Human) Eat() {
    fmt.Println("human eat......")
}
  • 由于 Human 被嵌入到 Hero 中,Hero 实例可以直接调用 Eat 方法:
go 复制代码
h := Hero{Human{"lucifer", "male"}, 80}
h.Eat() // 输出:human eat......

3. 方法重写

Go 语言允许外层结构体 重写 嵌入结构体的方法。例如:

go 复制代码
func (this *Hero) Eat() {
    fmt.Println("hero eat......")
}
  • Hero 重写了 Eat 方法后,调用 h.Eat() 时会优先调用 HeroEat 方法,而不是 HumanEat 方法:
go 复制代码
h.Eat() // 输出:hero eat......

4. 访问嵌入结构体的方法

即使外层结构体重写了方法,仍然可以通过 显式指定嵌入结构体 来调用原始方法。例如:

go 复制代码
h.Human.Eat() // 输出:human eat......

5. 初始化嵌入结构体

在初始化时,可以直接为嵌入结构体赋值。例如:

go 复制代码
h := Hero{Human{"lucifer", "male"}, 80}
  • 这里为 HeroHuman 部分初始化了 namesex 字段。

6. 代码示例分析
go 复制代码
func main() {
    h := Hero{Human{"lucifer", "male"}, 80} // 初始化 Hero
    h.doi()  // 调用 Human 的 doi 方法
    h.Eat()  // 调用 Hero 重写的 Eat 方法
    h.Fly()  // 调用 Hero 重写的 Fly 方法
}
  • h.doi():调用的是 Humandoi 方法,因为 Hero 没有重写它。
  • h.Eat()h.Fly():调用的是 Hero 重写后的方法。

注意点

  • Go 语言通过 结构体嵌入 实现类似继承的功能。
  • 嵌入结构体的字段和方法会被 自动提升 到外层结构体。
  • 外层结构体可以 重写 嵌入结构体的方法,实现多态。
  • 通过显式指定嵌入结构体,可以调用原始方法。

以下是关于 Go 语言多态 的学习笔记片段,基于你提供的代码,详细解释了多态的实现原理和代码分析。


Go 语言中的多态

多态是面向对象编程的重要特性之一,它允许 父类指针指向子类实例 ,并通过统一的接口调用不同子类的方法。Go 语言通过 接口 实现了多态。


1. 多态的实现条件

在你的代码中,多态的实现需要满足以下条件:

  1. 定义接口AnimalIF 接口定义了 SleepGetColorGetType 方法。
  2. 实现接口CatDog 结构体实现了 AnimalIF 接口的所有方法。
  3. 父类指针指向子类实例 :在 main 函数中,AnimalIF 类型的变量(父类指针)分别指向了 CatDog 的实例。

2. 代码分析
定义接口
go 复制代码
type AnimalIF interface {
    Sleep()
    GetColor() string // 获取动物颜色
    GetType() string  // 获取动物种类
}
  • AnimalIF 接口定义了三个方法:SleepGetColorGetType
  • 任何实现了这三个方法的类型都可以被认为是 AnimalIF 接口的实现。

实现接口

CatDog 结构体分别实现了 AnimalIF 接口的所有方法。

Cat 结构体
go 复制代码
type Cat struct {
    color string
}

func (this *Cat) Sleep() {
    fmt.Println("cat is sleeping")
}

func (this *Cat) GetColor() string {
    return this.color
}

func (this *Cat) GetType() string {
    return "Cat v1.0"
}
  • Cat 实现了 SleepGetColorGetType 方法。
Dog 结构体
go 复制代码
type Dog struct {
    color string
}

func (this *Dog) Sleep() {
    fmt.Println("dog is sleeping")
}

func (this *Dog) GetColor() string {
    return this.color
}

func (this *Dog) GetType() string {
    return "Dog v1.0"
}
  • Dog 也实现了 SleepGetColorGetType 方法。

使用多态

main 函数中,通过 AnimalIF 接口类型的变量调用不同子类的方法:

go 复制代码
func main() {
    var animal AnimalIF // 接口数据类型,父类的指针

    // 父指针指向 Cat 实例
    animal = &Cat{"green"}
    animal.Sleep() // 调用 Cat 的 Sleep 方法
    fmt.Println("Cat color:", animal.GetColor())
    fmt.Println("Cat type:", animal.GetType())

    // 父指针指向 Dog 实例
    animal = &Dog{"yellow"}
    animal.Sleep() // 调用 Dog 的 Sleep 方法
    fmt.Println("Dog color:", animal.GetColor())
    fmt.Println("Dog type:", animal.GetType())
}
  • animal = &Cat{"green"}AnimalIF 类型的变量 animal 指向 Cat 实例。
  • animal = &Dog{"yellow"}AnimalIF 类型的变量 animal 指向 Dog 实例。
  • 通过 animal.Sleep() 调用方法时,实际调用的是 CatDogSleep 方法,具体取决于 animal 指向的实例。

3. 运行结果

运行代码后,输出如下:

cat is sleeping
Cat color: green
Cat type: Cat v1.0
dog is sleeping
Dog color: yellow
Dog type: Dog v1.0
注意点
  • Go 语言通过 接口 实现多态。
  • 多态的条件:
    1. 定义接口。
    2. 子类实现接口的所有方法。
    3. 父类指针指向子类实例。
  • 多态提高了代码的复用性、扩展性和灵活性。

以下是关于 Go 语言中的 interface{} 的学习笔记片段,基于你提供的代码,详细解释了 interface{} 的使用方法和相关特性。


interface{}

interface{} 是 Go 语言中的 空接口,它可以存储任意类型的值。因为所有类型都至少实现了零个方法,所以所有类型都满足空接口。


1. 特点
  • 万能数据类型interface{} 可以存储任何类型的值,包括基本类型(如 intstring)和复杂类型(如结构体、切片、映射等)。
  • 类型断言 :通过 类型断言 ,可以从 interface{} 中提取具体的类型和值。

2. 代码分析
定义函数接受 interface{} 参数
go 复制代码
func myFunc(arg interface{}) {
    fmt.Println("myFunc is called")
}
  • myFunc 函数的参数类型是 interface{},因此它可以接受任意类型的参数。

类型断言

myFunc 函数中,使用 类型断言 判断 arg 的具体类型:

go 复制代码
value, ok := arg.(string)
if !ok {
    fmt.Println("arg is not string type")
} else {
    fmt.Println("arg is string type, value is", value)
}
  • arg.(string):尝试将 arg 断言为 string 类型。
  • value:如果断言成功,valuearg 的具体值。
  • ok:如果断言成功,oktrue;否则为 false

调用 myFunc 函数

main 函数中,分别传入不同类型的参数:

go 复制代码
func main() {
    book := Book{"golang"}

    myFunc(book)  // 传入结构体
    myFunc(100)   // 传入整数
    myFunc("200") // 传入字符串
}
  • myFunc(book):传入 Book 结构体实例。
  • myFunc(100):传入整数 100
  • myFunc("200"):传入字符串 "200"

3. 运行结果

运行代码后,输出如下:

myFunc is called
arg is not string type
myFunc is called
arg is not string type
myFunc is called
arg is string type, value is 200

4. 类型断言的详细说明

类型断言的语法:

go 复制代码
value, ok := arg.(Type)
  • arginterface{} 类型的变量。
  • Type:目标类型(如 stringint 等)。
  • value:如果断言成功,valuearg 的具体值;否则为 Type 的零值。
  • ok:如果断言成功,oktrue;否则为 false

5. 使用 switch 进行类型判断

除了 if-else,还可以使用 switch 语句对 interface{} 进行类型判断:

go 复制代码
func myFunc(arg interface{}) {
    fmt.Println("myFunc is called")

    switch v := arg.(type) {
    case string:
        fmt.Println("arg is string, value is", v)
    case int:
        fmt.Println("arg is int, value is", v)
    case Book:
        fmt.Println("arg is Book, auth is", v.auth)
    default:
        fmt.Println("arg is unknown type")
    }
}
  • arg.(type):只能在 switch 语句中使用,用于判断 arg 的具体类型。
  • varg 的具体值。

5. 注意点
  • interface{} 是 Go 语言中的万能数据类型,可以存储任意类型的值。
  • 通过 类型断言switch 类型判断 ,可以从 interface{} 中提取具体的类型和值。
  • 空接口在需要处理多种数据类型的场景中非常有用,例如 JSON 解析、泛型编程等。

好的!以下是关于 Go 语言中的 pair 概念 的学习笔记片段,基于你提供的代码,详细解释了 pair 的作用和实现原理。


pair

在 Go 语言中,pair 是一个常见的概念,用于描述变量或接口的内部结构。具体来说,pair 由两部分组成:

  1. 类型信息(Type):表示变量的静态类型。
  2. 值信息(Value):表示变量的实际值。

这种 pair 的概念在 Go 语言的接口实现中尤为重要,因为接口变量实际上存储了一个 pair,其中包含 动态类型动态值


1. 普通变量的 pair

对于普通变量,pair 的结构是固定的:

  • 类型信息:变量的静态类型。
  • 值信息:变量的实际值。
示例代码
go 复制代码
var a string
a = "abcde" // pair<static type: string, value: "abcde">
  • 变量 apair<string, "abcde">

2. 接口变量的 pair

对于接口变量,pair 的结构是动态的:

  • 类型信息:接口的动态类型(即实际存储的值的类型)。
  • 值信息:接口的动态值(即实际存储的值)。
示例代码
go 复制代码
var allType interface{}
allType = a // pair<dynamic type: string, value: "abcde">
  • 变量 allTypepair<string, "abcde">

  • 通过类型断言,可以从接口中提取出具体的类型和值:

    go 复制代码
    str, _ := allType.(string)
    fmt.Println(str) // 输出:abcde

3. 接口的类型断言

类型断言用于从接口中提取具体的类型和值。它的语法是:

go 复制代码
value, ok := interfaceVar.(Type)
  • interfaceVar:接口变量。
  • Type:目标类型。
  • value:如果断言成功,value 是接口的动态值。
  • ok:如果断言成功,oktrue;否则为 false
示例代码
go 复制代码
str, ok := allType.(string)
if ok {
    fmt.Println("动态类型是 string,值是", str)
} else {
    fmt.Println("动态类型不是 string")
}

4. 接口的 pair 在类型转换中的应用

在 Go 语言中,接口的 pair 可以用于实现类型转换。例如,将 io.Reader 接口转换为 io.Writer 接口。

示例代码
go 复制代码
tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
if err != nil {
    fmt.Println("open file error", err)
    return
}

var r io.Reader
r = tty // pair<dynamic type: *os.File, value: "/dev/tty">

var w io.Writer
w = r.(io.Writer) // pair<dynamic type: *os.File, value: "/dev/tty">

w.Write([]byte("hello test!\n"))
  • rpair<*os.File, "/dev/tty">
  • 通过类型断言,将 r 转换为 io.Writer 接口,wpair 仍然是 <*os.File, "/dev/tty">
  • 调用 w.Write 方法时,实际调用的是 *os.FileWrite 方法。

5. 注意点
  • pair 是 Go 语言中描述变量或接口内部结构的概念,包含类型信息和值信息。
  • 对于普通变量,pair 是静态的;对于接口变量,pair 是动态的。
  • 通过 类型断言,可以从接口中提取具体的类型和值。
  • 接口的 pair 在类型转换和方法调用中起着关键作用。

完整代码示例

以下是结合 pair 概念的完整代码示例:

示例 1:普通变量和接口的 pair
go 复制代码
package main

import "fmt"

func main() {
    var a string
    a = "abcde" // pair<static type: string, value: "abcde">

    var allType interface{}
    allType = a // pair<dynamic type: string, value: "abcde">

    str, _ := allType.(string)
    fmt.Println(str) // 输出:abcde
}
示例 2:接口的类型转换
go 复制代码
package main

import (
    "fmt"
    "io"
    "os"
)

func main() {
    tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
    if err != nil {
        fmt.Println("open file error", err)
        return
    }

    var r io.Reader
    r = tty // pair<dynamic type: *os.File, value: "/dev/tty">

    var w io.Writer
    w = r.(io.Writer) // pair<dynamic type: *os.File, value: "/dev/tty">

    w.Write([]byte("hello test!\n"))
}

以下是关于 Go 语言中的 reflect 的学习笔记片段,基于你提供的代码,详细解释了反射的使用方法和相关特性。


反射(reflect 包)

反射是 Go 语言中一种强大的机制,允许程序在运行时动态地获取变量的类型信息和值信息,并对其进行操作。reflect 包提供了实现反射的核心功能。


1. 反射的基本概念
  • 类型信息(Type):表示变量的静态类型。
  • 值信息(Value):表示变量的实际值。
  • 反射的核心函数
    • reflect.TypeOf(arg):获取 arg 的类型信息。
    • reflect.ValueOf(arg):获取 arg 的值信息。

2. 反射的基本用法
示例代码
go 复制代码
func reflectNum(arg interface{}) {
    fmt.Println("type:", reflect.TypeOf(arg))
    fmt.Println("value:", reflect.ValueOf(arg))
}
  • reflect.TypeOf(arg):返回 arg 的类型信息。
  • reflect.ValueOf(arg):返回 arg 的值信息。

3. 反射操作结构体
定义结构体
go 复制代码
type User struct {
    Id   int
    Name string
    Age  int
}

func (this *User) Call() {
    fmt.Println(this.Name)
}
  • User 结构体包含三个字段:IdNameAge
  • Call 方法是指针接收者方法。

反射获取结构体字段和方法
go 复制代码
func DoFiledAndMethod(input interface{}) {
    // 获取 type
    InputType := reflect.TypeOf(input)
    fmt.Println("InputType is : ", InputType)

    // 获取 value
    InputValue := reflect.ValueOf(input)
    fmt.Println("InputValue is : ", InputValue)

    // 如果 input 是指针类型,获取其指向的实际类型
    if InputType.Kind() == reflect.Ptr {
        InputType = InputType.Elem() // 获取指针指向的实际类型
        InputValue = InputValue.Elem() // 获取指针指向的实际值
    }

    // 通过 type 获取里面的字段
    for i := 0; i < InputType.NumField(); i++ {
        field := InputType.Field(i)
        value := InputValue.Field(i).Interface()
        fmt.Printf("%s: %v = %v\n", field.Name, field.Type, value)
    }

    fmt.Println("=======================")

    // 通过 type 获取里面的方法
    for i := 0; i < InputType.NumMethod(); i++ {
        m := InputType.Method(i)
        fmt.Printf("%s: %v\n", m.Name, m.Type)
    }
}
  • InputType.Elem():获取指针指向的实际类型。
  • InputValue.Elem():获取指针指向的实际值。
  • InputType.NumField():获取结构体的字段数量。
  • InputType.Field(i):获取第 i 个字段的信息。
  • InputValue.Field(i).Interface():获取第 i 个字段的值。
  • InputType.NumMethod():获取结构体的方法数量。
  • InputType.Method(i):获取第 i 个方法的信息。

4. 反射的注意事项
  1. 指针类型的处理

    • 如果 input 是指针类型,需要使用 InputType.Elem() 获取指针指向的实际类型。
    • 否则,NumFieldField 方法会报错。
  2. 方法获取的限制

    • 如果方法是指针接收者(例如 func (this *User) Call()),则必须传递指针给 reflect 包才能获取到方法。
    • 如果方法是值接收者(例如 func (this User) Call()),则可以传递值或指针。
  3. 性能开销

    • 反射操作比直接操作变量更慢,因此在性能敏感的场景中应谨慎使用。

结构体标签与 JSON 转换

在 Go 语言中,结构体标签是一种元数据,用于为结构体字段附加额外的信息。最常见的用途是 控制 JSON 编码和解码的行为


1. 结构体标签的基本语法

结构体标签是写在结构体字段后面的字符串,用反引号(`````)包裹。标签的格式通常是 key:"value",多个键值对之间用空格分隔。

示例
go 复制代码
type Movie struct {
    Title  string   `json:"title"`
    Year   int      `json:"year"`
    Price  int      `json:"rmb"`
    Actors []string `json:"actors"`
}
  • json:"title":表示在 JSON 编码和解码时,Title 字段对应的 JSON 键名为 "title"
  • json:"year":表示在 JSON 编码和解码时,Year 字段对应的 JSON 键名为 "year"
  • json:"rmb":表示在 JSON 编码和解码时,Price 字段对应的 JSON 键名为 "rmb"
  • json:"actors":表示在 JSON 编码和解码时,Actors 字段对应的 JSON 键名为 "actors"

2. JSON 编码(结构体 → JSON)

使用 encoding/json 包中的 json.Marshal 函数,可以将结构体编码为 JSON 字符串。

示例代码
go 复制代码
movie := Movie{"喜剧之王", 2000, 10, []string{"xingye", "lucifer"}}
jsonStr, err := json.Marshal(movie)
if err != nil {
    fmt.Println("json marshal error", err)
    return
}
fmt.Printf("jsonStr = %s\n", jsonStr)
输出结果
json 复制代码
jsonStr = {"title":"喜剧之王","year":2000,"rmb":10,"actors":["xingye","lucifer"]}
关键点
  • json.Marshal 会根据结构体标签将字段名映射为 JSON 键名。
  • 如果字段没有标签,默认使用字段名作为 JSON 键名。

3. JSON 解码(JSON → 结构体)

使用 encoding/json 包中的 json.Unmarshal 函数,可以将 JSON 字符串解码为结构体。

示例代码
go 复制代码
myMovie := Movie{}
err = json.Unmarshal(jsonStr, &myMovie)
if err != nil {
    fmt.Println("json unmarshal error", err)
}
fmt.Printf("%v\n", myMovie)
输出结果
go 复制代码
{喜剧之王 2000 10 [xingye lucifer]}
关键点
  • json.Unmarshal 会根据结构体标签将 JSON 键名映射为结构体字段名。
  • 如果 JSON 键名与结构体标签不匹配,该字段会被忽略。

4. 结构体标签的高级用法
忽略字段

如果希望某个字段在 JSON 编码时被忽略,可以使用 json:"-" 标签。

示例
go 复制代码
type Movie struct {
    Title  string   `json:"title"`
    Year   int      `json:"year"`
    Price  int      `json:"-"`
    Actors []string `json:"actors"`
}
  • Price 字段在 JSON 编码时会被忽略。
输出结果
json 复制代码
jsonStr = {"title":"喜剧之王","year":2000,"actors":["xingye","lucifer"]}

空值处理

如果希望某个字段在 JSON 编码时忽略空值,可以使用 omitempty 选项。

示例
go 复制代码
type Movie struct {
    Title  string   `json:"title"`
    Year   int      `json:"year,omitempty"`
    Price  int      `json:"rmb,omitempty"`
    Actors []string `json:"actors,omitempty"`
}
  • 如果 YearPriceActors 字段为零值(如 0nil),则在 JSON 编码时会被忽略。
输出结果
json 复制代码
jsonStr = {"title":"喜剧之王"}

5. 完整代码示例

以下是结合结构体标签和 JSON 转换的完整代码:

go 复制代码
package main

import (
    "encoding/json"
    "fmt"
)

// 结构体标签
type Movie struct {
    Title  string   `json:"title"`
    Year   int      `json:"year"`
    Price  int      `json:"rmb"`
    Actors []string `json:"actors"`
}

func main() {
    movie := Movie{"喜剧之王", 2000, 10, []string{"xingye", "lucifer"}}

    // 编码:结构体 ==> JSON
    jsonStr, err := json.Marshal(movie)
    if err != nil {
        fmt.Println("json marshal error", err)
        return
    }
    fmt.Printf("jsonStr = %s\n", jsonStr)

    // 解码:JSON ==> 结构体
    myMovie := Movie{}
    err = json.Unmarshal(jsonStr, &myMovie)
    if err != nil {
        fmt.Println("json unmarshal error", err)
    }
    fmt.Printf("%v\n", myMovie)
}

6. 运行结果

运行代码后,输出如下:

json 复制代码
jsonStr = {"title":"喜剧之王","year":2000,"rmb":10,"actors":["xingye","lucifer"]}
{喜剧之王 2000 10 [xingye lucifer]}

相关推荐
枫叶丹418 分钟前
【HarmonyOS之旅】基于ArkTS开发(三) -> 兼容JS的类Web开发(三)
开发语言·前端·javascript·华为·harmonyos
SomeB1oody31 分钟前
【Rust自学】16.3. 共享状态的并发
开发语言·后端·rust
西猫雷婶35 分钟前
python学opencv|读取图像(四十七)使用cv2.bitwise_not()函数实现图像按位取反运算
开发语言·python·opencv
专职38 分钟前
spring boot中使用spring-security案例
spring boot·后端·spring
背太阳的牧羊人1 小时前
分词器的词表大小以及如果分词器的词表比模型的词表大,那么模型的嵌入矩阵需要被调整以适应新的词表大小。
开发语言·人工智能·python·深度学习·矩阵
wanghao6664552 小时前
Java基础面试题总结(题目来源JavaGuide)
java·开发语言
码界筑梦坊3 小时前
基于Django的豆瓣影视剧推荐系统的设计与实现
后端·python·django·毕业设计
HYUJKI3 小时前
自定义注解
java·开发语言·spring
fmdpenny3 小时前
前后分离Vue3+Django 之简单的登入
后端·python·django
步、步、为营3 小时前
C#牵手Blazor,解锁跨平台Web应用开发新姿势
开发语言·前端·c#