Go基础 语法(三)

128 阅读17分钟

Go接口与类型定义

接口定义

基本语法:type Name interface{}

这个东西 课程中 用到了很多后边才讲的东西,比如 指针 接收器;

这门课在这讲的听着很难受 因为这个与ts的interface差别太大,感觉比ts的抽象太多了(:

面向接口编程,需要实践中理解,所以这里只能查chatgpt先理解个大概,等课程里讲到后边 越用越多 才能有更深的理解 (:

暂时理解:type interface 就是把一些方法的签名组合到一起 或者 说是一些方法集合,但是方法具体的实现由实现接口者自己来定义,只要你符合接口中的函数签名就行。

就是说接口让你接受一个sting参数返回值也是一个string,那么你就必须是string参数 return string 才算一个合格的接口实现者。 也可以从抽象类的角度来想象 ----> 继承抽象类 完全实现抽象类中的函数签名,具体实现自行决定,只要保证函数签名相同,否则就会报错 ----> 实现一个接口 完全实现接口中的函数签名,具体实现自行决定,只要保证函数签名相同,否则就会报错

当然这是暂时理解:后续会根据学到的知识,不断加深理解

接口里面只能有方法,方法也不需要加func关键字

啥是接口?:接口时一组行为的抽象(或者说 行为规范)--->尽量使用接口,以实现面向接口编程 在 Go 语言中,定义一个接口并实现它的所有方法是一种常见的做法。接口定义了一组方法签名,而任何类型只要实现了这些方法,就被认为满足了该接口。以下是一个具体示例来展示这个过程:

定义接口

假设我们有一个名为 Animal 的接口,它定义了两个方法:Speak()Move()

goCopy code
package main

import "fmt"

// Animal 接口
type Animal interface {
    Speak() string
    Move() string
}

// Dog 类型
type Dog struct{}

// Dog 类型实现 Animal 接口的 Speak 方法
func (d Dog) Speak() string {
    return "Woof"
}

// Dog 类型实现 Animal 接口的 Move 方法
func (d Dog) Move() string {
    return "Run"
}

func main() {
    var animal Animal = Dog{}
    fmt.Println("The dog says:", animal.Speak())
    fmt.Println("The dog:", animal.Move())
}

在这个示例中:

  1. 我们定义了一个 Animal 接口,它要求实现两个方法:SpeakMove
  2. 然后我们定义了一个 Dog 类型,并为这个类型实现了这两个方法。
  3. main 函数中,我们创建了一个 Dog 实例,并将其赋值给一个 Animal 类型的变量。由于 Dog 实现了 Animal 的所有方法,这是允许的。
  4. 我们随后调用了 SpeakMove 方法,展示了多态性的使用。

通过这种方式,Go 的接口提供了一种强大的方法来编写模块化和可维护的代码。它允许你根据抽象类

结构体定义

type Name struct {
  name type
}

结构体和结构体的字段都遵循大小写控制访问性的原则 (同一dir下是共享作用域的,大小写是为了实现不同dir的访问) 通过 . 这个符号来访问字段或者方法 按js来说就是属性 eg:person.age math.add()

结构体初始化

  1. Go没有构造函数
  2. 初始化语法 Struct{}
  3. 获取指针
    • &StructName{}
    • new(StructName) new可以理解为Go会为你的变量分配内存,并且把内存都置为 默认值 字段初始化:User{Name:"胖胖",age:12}
type User struct {
   name string
   age  uint16
}

func main() {
   //u1 是指向一个User 对象的指针
   u1 := &User{}
   u1.age = 12
   
   println(u1)      //0xc00004bf18
   
   fmt.Println(*u1) //{ 12}
   
   //   u2的字段都是 默认值
   
   u2 := User{}
   
   //println 函数主要用于基本类型的打印,对于复杂的类型(如结构体)不太适用
   //println(u2)//illegal types for operand: print
   
   fmt.Println(u2) // 打印结构体内容 { 0}
   
   u2.name = "胖胖"
   fmt.Println(u2) // 打印结构体内容{胖胖 0}
   u2.age = 12
   fmt.Println(u2) // 打印结构体内容{胖胖 12}
   
   // 如果你直接使用指针  newU === nil
   var newU *User
   println(newU)//0x0
   fmt.Println(newU) // 打印结构体内容<nil>  

   newU = new(User)
   println(newU)     //0xc0000080a8
   //`fmt.Println` 打印一个指针变量时,Go 会显示指针的地址,并在类型前面加上 `&` 符号来表明这是一个地址的引用。
   fmt.Println(newU) // 打印结构体内容&{ 0}

}

未初始化的指针就是 nil &取地址 *解地址(实际值) 先简单理解一下指针:指针主要依赖两个操作符 & * 我们可以想象 :

想象一下,有一个盒子(代表一个变量),里面装着一个苹果(代表变量的值)。现在,我们想知道这个盒子在仓库中的位置(即变量的内存地址),并且还想能够通过这个位置找到盒子,从而取出苹果

& 操作符:找到盒子的位置

  • 操作符:通过位置找到盒子,并取出里面的内容

u1 := &User{} User{} 先创建了一个 结构体实例(盒子) &User{}使用&符号,找到变量的内存地址(找到盒子的位置)

fmt.Println(*u1)通过u1内存地址(位置)找到u1(盒子),并取出里面的内容(实例中的数据)

继续:

在 Go 语言中,当你声明一个指向某类型的指针但没有初始化时,该指针的默认值是 nil。这个特性是 Go 语言设计中的一部分,它确保指针在未初始化时具有一个明确的、一致的值,即 nil

指针变量的 nil

在你的例子中,newU 被声明为指向 User 类型的指针,但在声明时并没有初始化:

goCopy code
var newU *User
fmt.Println(newU) // 打印 <nil>

由于 newU 没有被初始化,它的值是 nil。这表示它目前没有指向任何 User 类型的实例。

为什么不是报错?

在很多编程语言中,指针如果未初始化通常会指向一个不确定的内存地址,这可能导致未定义行为,甚至程序崩溃。而在 Go 中,未初始化的指针默认为 nil,这是一种安全机制,可以防止指向不确定或无效的内存地址。nil 指针表示“没有值”或“没有指向任何东西”,这比指向一个随机或无效的内存地址要安全得多。

使用 nil 指针的注意事项

尽管 nil 指针比随机内存地址安全,但尝试通过 nil 指针访问或修改其指向的数据会导致运行时错误(panic)。因此,在对指针进行解引用前,检查它是否为 nil 是一个好习惯:

goCopy code
if newU != nil {
    // 安全地使用 newU
}

初始化指针

如果你想让 newU 指向一个 User 类型的实例,你可以通过 new 函数或使用 & 操作符进行初始化:

goCopy code
newU = new(User)  // 使用 new 函数
// 或
newU = &User{}    // 使用 & 操作符

这样,newU 就会指向一个新的 User 实例,而不再是 nil

总结

在 Go 语言中,未初始化的指针默认为 nil,这是一种设计选择,以确保安全和一致性。虽然 nil 指针不会导致程序崩溃,但在解引用之前应该始终检查它们是否为 nil

方法接收器

一个方法可以定义在这个结构体上,也可以定义在这个结构体的指针上。 在 Go 语言中,定义方法时可以选择使用值接收器或指针接收器,这两种方式在方法作用于接收器时有着本质的区别。

值接收器

  • 作用:当你使用值接收器时,方法接收的是接收器类型的一个副本。在这个例子中,ChangeName 方法接收的是 User 结构体的副本。
  • 修改:在 ChangeName 方法内部对 u 所做的任何修改都只会影响这个副本,不会影响原始的结构体实例。
  • 使用场景:当你不需要修改原始结构体或者希望每次方法调用都使用接收器的新副本时,使用值接收器是个不错的选择。

指针接收器

  • 作用:使用指针接收器时,方法接收的是指向接收器类型的指针。在这个例子中,Sing 方法接收的是指向 User2 结构体的指针。
  • 修改:在 Sing 方法内部对 u 所做的任何修改都会直接影响到指针指向的原始结构体。这意味着你可以通过方法修改结构体的状态。
  • 使用场景:当你需要在方法中修改原始结构体的数据,或者结构体太大,传递副本代价较高时,使用指针接收器更合适。

总结

值传递与引用传递的区别

  • 值接收器:方法接收结构体的副本,对副本的修改不会影响原始结构体。
  • 指针接收器:方法接收结构体的指针,对指针指向的数据的修改会影响原始结构体。

选择使用哪种类型的接收器取决于你是否需要在方法内修改原始结构体,以及考虑到结构体的大小。对于大型结构体或需要修改原始数据的情况,通常推荐使用指针接收器。

代码示例

package main

import "fmt"

type User struct {
    name string
    age  uint16
}
type User2 struct {
    name string
    age  uint16
}
type Person interface {
    Sing()
    ChangeName(newName string) string
}

// 值接收器
func (u User) Sing(word string) {
    println(word)
}
func (u User) ChangeName(newName string) string {
    fmt.Printf("ChangeName的地址 %p \n",&u)
    u.name = newName
    return newName
}

// 指针接收器
func (u *User2) Sing(word string) {
    println(word)
}
func (u *User2) ChangeName(newName string) string {
    u.name = newName
    return newName
}
func main() {
    var user1 = User{}
    fmt.Printf("user1的地址 %p \n",&user1) //ChangeName的地址 0xc000008078  user1 0xc000008048
    fmt.Println(user1) //{ 0} <------------
                                          |
    user1.Sing("小酒窝长睫毛")            |
    user1.ChangeName("胖胖1号")           |
                                         |
    fmt.Println(user1) //{ 0} <-----------值未被改变

    var user2 = new(User2)
    fmt.Println(user2) //&{ 0}

    user2.Sing("美丽的无可救药")
    user2.ChangeName("胖胖2号")

    fmt.Println(user2) //&{胖胖2号 0} // 值被改变

    //var user2Copy = User2{}
    //user2Copy.Sing("胖胖singing") 
}

Go 语言中所有的函数参数都是通过值传递的,除非你显式地使用指针。 不同于js(基础类型值传递,引用类型引用传递)

//这样就成为了 地址传递
func main() {
    a := 1
    change(&a)
    println(a)
}
func change(a *int) {
    *a = 12
}

如果是 切片那 如果是 unsafe那?

切片参数传递

切片在 Go 中以值传递的形式传递,但需要注意的是,切片本身实际上是一个包含指向底层数组的指针、长度和容量的引用类型。因此,当你将一个切片传递给函数时,传递的是切片的副本,但这个副本仍指向相同的底层数组。

这意味着,如果你在函数内部修改切片的元素,这些更改将影响到原始切片。但是,如果你改变切片的长度或容量(例如通过重新切片操作),这些改变只会影响函数内部的切片副本。

goCopy code
func modifySlice(s []int) {
    s[0] = 10
}

func main() {
    a := []int{1, 2, 3}
    modifySlice(a)
    fmt.Println(a) // 输出 [10 2 3]
}

在这个例子中,modifySlice 函数修改了切片的第一个元素,这个改变反映在了原始的切片上。

unsafe 包参数传递

使用 unsafe 包可以绕过 Go 语言的类型安全,进行一些低级的操作,如直接操作内存。使用 unsafe 包需要谨慎,因为它可以破坏程序的安全性和稳定性。

当你通过 unsafe 操作传递参数时,你实际上是在操作底层内存。这种操作的结果取决于你如何使用它。使用 unsafe 包可以直接读取或修改内存地址处的数据,这意味着你可以直接改变任何变量的值,不受类型限制。

goCopy code
func main() {
    var a int = 5
    var b *int = &a
    var c uintptr = uintptr(unsafe.Pointer(b))
    var d *int = (*int)(unsafe.Pointer(c))

    *d = 10
    fmt.Println(a) // 输出 10
}

在这个例子中,我们使用 unsafe 包直接操作了变量 a 的内存。

总结

  • 切片是引用类型,虽然它们以值传递的形式传递给函数,但由于它们包含指向底层数组的指针,因此函数内部的更改可以影响原切片。
  • 使用 unsafe 包允许进行低级的内存操作,这可以改变任何类型的变量,包括那些不是通过指针传递的。但使用 unsafe 需要非常小心,因为它可以导致程序不安全或不稳定

结构体自引用

在 Go 语言中,结构体的自引用是指结构体中有一个字段,其类型是该结构体本身的指针类型。这种结构在构建某些类型的数据结构时非常有用,例如链表、树等。

自引用结构体的示例

以下是一个简单的自引用结构体的例子:

package main

import "fmt"

type ListNode struct {
    Value int
    Next  *ListNode // 自引用,指向另一个 ListNode 的指针
}

func main() {
    // 创建链表:1 -> 2 -> 3
    node1 := &ListNode{Value: 1}
    node2 := &ListNode{Value: 2}
    node3 := &ListNode{Value: 3}

    node1.Next = node2
    node2.Next = node3

    // 遍历链表并打印
    for node := node1; node != nil; node = node.Next {
        fmt.Println(node.Value)
    }
    &{Value:1 Next:0xc000024080}
    &{Value:2 Next:0xc000024090} 
    &{Value:3 Next:<nil>} 还未被初始化 
}

在这个例子中,ListNode 结构体表示链表中的一个节点。它包含一个 Value 字段和一个指向下一个 ListNode 的指针字段 Next。通过 Next 字段,每个 ListNode 实例都可以引用链表中的下一个节点,从而形成一个链表。

自引用结构体的用途

自引用结构体常用于实现各种复杂的数据结构,如:

  • 链表:每个节点包含数据和指向下一个节点的指针。
  • :比如二叉树,其中每个节点可能有指向其子节点的指针。
  • :节点可能有指向其他多个节点的指针。

注意事项

使用自引用结构体时,需要注意可能出现的循环引用问题。循环引用会导致无法正确释放内存,进而引发内存泄漏。在实现这类结构时,应确保适当管理节点的生命周期,特别是在涉及到删除或移除节点时。

在 Go 语言中,可以使用 type 关键字来定义新的类型。当你使用形如 type TypeA TypeB 的语句时,你实际上是在创建一个已有类型(TypeB)的新类型(TypeA)。这种新类型称为衍生类型,它继承了原始类型的底层结构,但会被视为一个完全不同的类型。

衍生类型的特点

  • 独立类型:尽管衍生类型在内部结构上与原始类型相同,但在类型系统中它们是完全独立的。
  • 保留底层特性:衍生类型保留了原始类型的底层特性,比如整数的运算特性、切片的索引特性等。

示例

假设我们有一个基本类型 int,我们可以创建一个基于 int 的新类型 MyInt

goCopy code
type MyInt int

在这个例子中,MyInt 是基于 int 创建的新类型。尽管 MyInt 在内部表现与 int 相同(例如,它可以进行加减等整数运算),但它是一个独立的类型。这意味着你不能直接将 MyInt 类型的变量赋值给 int 类型的变量,反之亦然,除非通过显式类型转换。

类型转换

要在 MyIntint 之间进行转换,需要进行显式类型转换:

goCopy code
var a int = 5
var b MyInt = MyInt(a)  // 将 int 转换为 MyInt
var c int = int(b)      // 将 MyInt 转换回 int

为什么使用衍生类型

  • 类型安全:通过创建新的类型,可以避免不同类型的值被意外混用。这对于提高代码的安全性和可读性非常有用。
  • 添加额外语义:新类型可以对原始类型提供额外的上下文或语义。例如,使用 type UserID inttype OrderID int 作为不同的类型可以帮助区分用户 ID 和订单 ID,尽管它们都是整数。
  • 方法附加:在 Go 中,你可以为自定义类型添加方法,这对于原始类型是不可能的。

***如果 TypeB 是一个结构体,那么 type TypeA TypeB 创建的 TypeA 是一个基于 TypeB 结构体的新类型。这种方式使 TypeA 拥有与 TypeB 相同的结构和行为,但在类型系统中,它们是两个不同的类型。

示例

考虑以下的 Go 代码:

goCopy code
type Person struct {
    Name string
    Age  int
}

// 基于 Person 结构体创建新的类型 Employee
type Employee Person

在这个例子中:

  • Person 是一个原始的结构体类型,包含 NameAge 字段。
  • Employee 是基于 Person 结构体创建的新类型。Employee 有与 Person 相同的字段和内部结构,但它是一个独立的类型。

类型间的操作

尽管 EmployeePerson 在结构上是相同的,但你不能直接将 Employee 类型的变量赋值给 Person 类型的变量,反之亦然,除非通过显式类型转换。

goCopy code
var p Person = Person{"Alice", 30}
var e Employee = Employee(p)  // 将 Person 类型转换为 Employee 类型

为什么使用这种方式

这种创建新类型的方式可以帮助你在逻辑上区分不同的概念,即使它们在结构上是相同的。例如,在你的系统中,可能需要区分普通的人(Person)和雇员(Employee),即使它们拥有相同的属性。

通过这种方式,你可以为 Employee 类型添加特定的方法,而不影响 Person 类型。这样做可以增强代码的可读性和类型安全性,使得代码更容易维护和理解。

方法和接收器

你可以为这些衍生类型定义特定的方法:

goCopy code
func (e Employee) Promote() {
    // 特定于 Employee 的方法
}

在这个例子中,即使 EmployeePerson 在结构上相同,Promote 方法只能被 Employee 类型的变量调用。

总结

在 Go 中,使用 type 关键字从现有的结构体创建新类型是一种有效的方式来增强类型安全和明确类型的意图。尽管新旧类型在内部结构上相同,它们在类型系统中是完全独立的。这样做可以帮助你更清晰地表示程序的逻辑和设计意图。

组合

组合不是继承没有多态,没有像solidity中的 虚函数 实函数

插播

在 Go 语言中,很多 Node.js 提供的功能可以通过标准库或第三方库找到对应的实现。以下是一些 Node.js 模块在 Go 中的对应项:

  1. fs (文件系统操作) :

    • 在 Go 中,文件系统操作主要通过 osiopath/filepath 等包进行。例如,os 包用于文件的创建、打开、读写、权限设置等。
  2. process (进程信息) :

    • Go 的 os 包提供了与系统进程交互的功能,比如获取环境变量(os.Getenv),执行程序(os.StartProcess),以及其他进程相关信息。
  3. spawn 和 exec (执行子进程) :

    • 在 Go 中,os/exec 包用于运行外部命令。它提供了类似于 Node.js 的 spawnexec 功能。
  4. os (操作系统特定信息) :

    • Go 的 os 包提供了平台无关的接口以及操作系统特定信息的访问。
  5. child_process (子进程管理) :

    • 正如上面提到的,Go 的 os/exec 包用于管理子进程,包括启动、通信和处理输出。
  6. stream (流处理) :

    • 在 Go 中,流处理通常通过 io 包实现,它提供了一组用于 I/O 原语的接口和实现,比如 ReaderWriter 接口。
  7. http (HTTP 客户端和服务器) :

    • Go 的 net/http 包非常强大,提供了 HTTP 客户端和服务器的实现。你可以用它来创建 HTTP 服务器(http.ListenAndServe)和发起 HTTP 请求(http.Gethttp.Post 等)。
  8. net (网络操作) :

    • Go 的 net 包提供了网络 I/O 的底层接口,包括 TCP/IP、UDP、域名解析等功能。

Go 语言的标准库非常全面,对于大多数常见的编程任务提供了良好的支持。而对于更特定的需求,Go 社区还提供了大量的第三方库。