Go——面向对象

一. 匿名字段

        go支持只提供类型而不写字段名的方式,也就是匿名字段,也称为嵌入字段。

  • 同名字段的情况

  • 所以自定义类型和内置类型都可以作为匿名字段使用

  • 指针类型匿名字段

二.接口

        接口定义了一个对象的行为规范,但是定义规范不实现,由具体的对象实现规范的细节。

        2.1 接口类型

        在Go语言中接口(interface)是一种类型,一种抽象类型。

        interface是一组method的集合,接口做的事情就像定义一个协议,不关心属性(数据),只关心行为(方法)。

        2.2 为什么使用接口

        查看下面的图片,只是由于类型不同,就需要定义两个逻辑一样的函数。如果后面出现了其它动物,也会需要定义函数。

        Go语言为了解决类似上面的问题,就设计了接口这个概念。接口区别于我们之前所有的具体类型,接口是一个抽象类型。当你看到一个接口类型是,你不知道他是什么,唯一知道的是通过它的方法能做什么。 

        2.3 接口定义

        Go语言提倡面向接口编程。

  • 接口在底层实现上包含两部分,即类型(type)和数据(data)。
  • 接口是一个或多个方法签名的集合
  • 任何类型的方法集中只要拥有该接口对应的全部方法,就表示它实现了该接口,无须在该类型上显示声明实现了那个接口。这称为Structural Typing。
  • 所谓对应方法,是指有相同名称,参数列表(不包括参数名)以及返回值。
  • 当然,该类型还可以有其它方法。
  • 接口只有方法声明,没有实现,没有数据字段(属性)
  • 接口可以匿名嵌入其它接口,或者嵌入到其它结构中。
  • 对象赋值给接口时,会发生拷贝,而接口内部存储的是指向这个复制品的指针,即无法修改复制品的状态,也无法获取指针。即如果对象是结构体或者基本数据类型,它会被值拷贝到接口中。如果对象是指针类型,这个指针指向的结构体实现了接口,那么接口中存储的是指针的副本,而不是指针本身。
  • 只有当接口存储的类型和对象都为nil时,接口才为nil。
  • 接口调用不会做receiver的自动转换。
  • 接口同样支持匿名字段方法。
  • 接口可以实现类似面向对象编程(OOP)的多态。
  • 空接口可以作为任何类型数据的容器。空接口是没有声明任何方法的接口。但是,也无法通过空接口来调用对象的方法或访问其属性。
  • 一个类型可以实现多个接口。
  • 接口命名习惯以er结尾。

        每一个接口由数个方法组成,接口的定义格式如下:

type 接口类型名 interface{
    方法1(参数列表1)返回值列表1
    方法2(参数列表2)返回值列表2
    ...
}

        其中:

  • 接口名:使用type将接口名定义为自定义的类型名。Go语言的接口在命名时,一般会在单词后面添加er,接口名最好要能突出该接口的类型含义。
  • 方法名:当方法名首字母是大写且接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
  • 参数列表和返回值列表:参数列表和返回值列表中的参数变量名可以省略。

        举个例子:

type writer interface{
    Write([]byte) error
}

        当看到这个接口的时候,你不知道他是什么,唯一知道的是可以通过它的Write方法来做一些事情。

        2.4 实现接口的条件

        一个对象只要全部实现了接口的方法,那么就实现了这个接口。换句话说,接口就是一个需要实现的方法列表。

package main

import "fmt"

type Sayer interface {
	Say()
}

type Cat struct{}

//实现了Sayer接口
func (c *Cat) Say() {
	fmt.Println("喵喵喵...")
}

type Dog struct{}

//实现了Sayer接口
func (d *Dog) Say() {
	fmt.Println("汪汪汪...")
}

        2.5 接口类型变量

        接口类型变量能够存储所有实现了该接口的实例。

        需要指针传入是因为方法的receiver为指针类型,对象的指针类型(*T)的方法集为值类型(T)和指针类型(*T)。

        2.6 值接收者和指针接收者实现接口的区别

        值接收者和指针接收者实现接口区别在于:方法集的不同。接口接收值类型(T)对象方法集为值接收者(T)实现的方法。接口接收指针类型对象(*T)方法集为值接收者(T)和指针接收者(*T)现象的方法。

  • 值接收者

  •  指针接收者

        2.7 类型与接口的关系

        2.7.1 一个类型实现多个接口

        一个类型可以同时实现多个接口,而接口间彼此独立,不知道对方的实现。

                2.7.2 多个类型实现同一个接口

        Go语言中不同的类型还可以实现同一接口。

        一个接口的方法不一定需要由一个类型完全实现,接口的方法可以通过在类型中嵌入其它类型或者结构体来实现。

        2.7.3 接口嵌套

         接口与接口之间可以通过嵌套创造出新的接口。

type Sayer interface {
	Say()
}

type Mover interface {
	Move()
}

type Animal interface {
	Sayer
	Mover
}

        嵌套的接口的使用和普通接口一样,这里实现嵌套的接口。

        2.8 空接口

        2.8.1 空接口的定义

        空接口是指没有定义任何方法的接口。因此任何类型都实现了空接口。

        空接口类型变量可以存储任意类型的变量。

        2.8.2 空接口的应用

  • 空接口作为函数参数

        使用空接口实现可以接收任意类型的函数参数。

  • 空接口作为map的值

        使用空接口实现可以保存任意值的字典。

         2.8.3 类型断言

        空接口可以存储任意类型的值,那我们如何获取其存储的具体数据呢?

  • 接口值

        一个接口的值(简称接口值)是由一个具体类型和具体类型的值俩个部分组成。这两部分分别称为接口的动态类型和动态值。

        我们来看一个例子:

package main

import (
	"bytes"
	"io"
	"os"
)

func main() {

	var w io.Writer
	w = os.Stdout
	w = new(bytes.Buffer)
	w = nil
}

        图解: 

        想要判断空接口中的值这个时候就可以使用类型断言,其语法格式:

x.(T)

        其中:

  • x:表示类型为interface{}的变量
  • T :表示断言x可能是的类型

        该语法返回两个参数,第一个参数是x转化为T类型后的变量,第二个是布尔值,若为true表示断言成功,为false则表示断言失败。

        举个例子:

        自定义类型: 

        我们还可以使用switch语句来实现多个类型的断言:

        因为空接口可以存储任意类型值的特点,所以空接口在Go语言中使用十分广泛。

         但是接口需要注意的是,只有当有两个或者两个以上的具体类型必须以相同的方式进行处理时才需要定义接口。不要为了接口而写接口,那样会增加不必要的抽象,导致不必要的运行时消耗。

三. 接口的底层实现

  • 案例
package main

type EInterface interface{}
type IInterface interface {
	Do()
}

type IInterfaceImpl struct{}

func (imp1 IInterfaceImpl) Do() {

}

func main() {
	var impl1 EInterface = IInterfaceImpl{}
	var impl2 IInterface = IInterfaceImpl{}
	println(impl1)
	println(impl2)
}

        3.1 数据结构

        golang中的接口非为带方法的接口和不带方法的空接口,带方法的接口在底层使用iface表示,空接口的底层则是eface表示。

  • eface

        eface是空接口类型的底层实现,源码如下:

type eface struct {
    _type *_type
    data  unsafe.Pointer
}

两个字段都是指针类型,含义分别是:

  • _type:指向实际的类型。上面案例是IInterfaceImpl
  • data:指向实际的值。上面案例是IInterfaceImpl结构体的值

        注意:var v interface{} = (*int)nil,变量v其实使用的eface结构表示。其中_type的类型对应的是int类型的指针,而data部分为nil,所以整体变量v != nil。 

  • iface

        iface是非空接口类型的底层实现,源码如下:

type iface struct {
    tab  *itab
    data unsafe.Pointer
}

        上面的案例impl2就是非空接口类型的变量,两个字段也是指针类型,含义分别是:

  • tab:指向itab结构体,itab结构体存储了接口所有方法列表。
  • data:指向对应的值。上面案例为IInterfaceImpl结构体的值。 

  • _type结构

        该结构于golang的类型系统有关,无论是内置类型还是自定义数据类型,都用_type结构表示其元信息。

type _type struct {
    size       uintptr
    ptrdata    uintptr // size of memory prefix holding all pointers
    hash       uint32
    tflag      tflag
    align      uint8
    fieldAlign uint8
    kind       uint8
    // function for comparing objects of this type
    // (ptr to object A, ptr to object B) -> ==?
    equal func(unsafe.Pointer, unsafe.Pointer) bool
    // gcdata stores the GC type data for the garbage collector.
    // If the KindGCProg bit is set in kind, gcdata is a GC program.
    // Otherwise it is a ptrmask bitmap. See mbitmap.go for details.
    gcdata    *byte
    str       nameOff
    ptrToThis typeOff
}

        以int32类型的指针为例,具体的字段含义及其作用如下:

  • size:类型的大小(字节数)。*int32类型的大小是64位系统下8字节,32位系统下4字节。
  • ptrdata:所有指针内存前缀大小,指向int32类型的实例。
  • hash:类型的哈希值,即_type.hash。
  • tflag:类型标记,表示类型的特性。
  • align:类型的对齐方式,*int32类型32位系统下按4字节对齐,64位系统下8字节对齐。
  • fieldAlign:字段对齐方式,*int32类型的字段对齐方式 32位系统下按4字节对齐,64位系统下8字节对齐。
  • kind:类型的种类。用于区分基本类型,结构体,接口等。
  • equal:比较函数,用于区分两个*int32类型的实例是否相等。
  • gcdata:与GC有关。
  • str:类型名称的偏移。
  • ptrToThis:指向该类型的指针。

         _type是一个很复杂的结构,这里只需要知道,通过该结构能获取到结构体实现的所有方法。

        下面是iface.go中的init方法中的一段代码,_type结构的uncommon方法会返回一个指针,在此基础上加一个偏移量(moff)就能得到实际结构体实现的方法列表。

func (m *itab) init() string {
    inter := m.inter
    typ := m._type
    x := typ.uncommon()

    // both inter and typ have method sorted by name,
    // and interface names are unique,
    // so can iterate over both in lock step;
    // the loop is O(ni+nt) not O(ni*nt).
    ni := len(inter.mhdr)
    nt := int(x.mcount)
  // 实际类型的方法数组
    xmhdr := (*[1 << 16]method)(add(unsafe.Pointer(x), uintptr(x.moff)))[:nt:nt]
  ...
}
  • itab结构

        itab结构是golang非空接口iface中一个非常重要的字段,类型的赋值,断言等都离不开该字段。

type itab struct {
  // 接口类型的指针,比如对于 io.Reader 接口,记录的是接口类型的信息(如接口定义的方法,Read 方法)
    inter *interfacetype
  // 实际结构类型的指针,记录的是实际类型的信息,比如 os.File 类型,实现了 io.Reader 接口
    _type *_type
    hash  uint32 // copy of _type.hash. Used for type switches.
    _     [4]byte
  // 变长数组,fun[0]==0 表示 _type 没有实现 inter 接口
    fun   [1]uintptr
}

type interfacetype struct {
  // 接口类型元信息
    typ     _type
  // 包路径
    pkgpath name
  // 接口的所有方法列表
    mhdr    []imethod
}

        itab结构的字段含义:

  • inter记录的是非空接口类型的元信息,其中mhdr是接口的方法表
  • _type记录的是实际类型的指针,即实现接口的类型
  • fun保存的是实际类型中实现的方法的地址。当fun[0] == 0时,表示该类型没有实现该接口;当fun[0]!=0时,则代表该类型实现了接口的所有方法,这时候就可以通过偏移量调用具体类型对象的方法。

        3.2 itab关键方法

        通过上述对itab结构的描述,不难理解,itab其实就是一个缓存,用于快速判断具体类型是否实现了某个接口。

        一般情况下,如果要判断需要对接口的具体类型的方法集进行比较。当如果每次都这样比较,效率会很低。通过将比较结果缓存起来,下次再判断的时候,就能直接根据itab快速得出结论了。

        go源码的runtime里定义了全局变量itebTable,用户缓存itab。

// 用于缓存 itab
itabTable     = &itabTableInit
itabTableInit = itabTableType{size: itabInitSize}

// 全局的 itab 表
type itabTableType struct {
    size    uintptr             // entries 的长度
    count   uintptr             // 当前 entries 的数量,即 itab 数量
    entries [itabInitSize]*itab // 保存 itab 的哈希表
}

        这里其实是一个全局的哈希表,哈希表的key就是interfacetype + _type,value就是对应的itab。

        判断某个类型是否实现了接口时,只需要传入接口的接口类型interfacetype和实际类型_type即可:

  • 如果在itabTable中没有找到对应的itab,则需要依次比较方法集,生成itab并缓存到itabTable中
  • 如果找到了对应的itab,则判断func[0],如果等于0则说明该类型没有实现该接口 

        有了哈希表,还要考虑如何向其中添加数据和获取数据,也就是下面两个方法。

  • getitab

        该函数的作用是:通过interfacetype和_type,也就是接口类型和实际结构类型,从表中获取对应itab。

func getitab(inter *interfacetype, typ *_type, canfail bool) *itab {
  ...
    var m *itab

    // 尝试从 itabTable 表中获取 itab,获取到直接返回
    t := (*itabTableType)(atomic.Loadp(unsafe.Pointer(&itabTable)))
    if m = t.find(inter, typ); m != nil {
        goto finish
    }

    // 没有找到,获取锁再次查找
    lock(&itabLock)
    if m = itabTable.find(inter, typ); m != nil {
        unlock(&itabLock)
        goto finish
    }

    // 如果 itabTable 中没有找到,则新建一个 itab,并调用 itabAdd 将其缓存到 itabTable 中
    m = (*itab)(persistentalloc(unsafe.Sizeof(itab{})+uintptr(len(inter.mhdr)-1)*goarch.PtrSize, 0, &memstats.other_sys))
    m.inter = inter
    m._type = typ
    m.hash = 0
    m.init()
    itabAdd(m)
    unlock(&itabLock)
finish:
  // m.fun[0] != 0 表示该类型实现了接口的所有方法,可以返回 itab
    if m.fun[0] != 0 {
        return m
    }
  //canfail 用于控制类型转换失败的行为。比如 v := s.(Dst),这里的 canfail == false,那么在断言失败时,会 panic
    if canfail {
        return nil
    }
    panic(&TypeAssertionError{concrete: typ, asserted: &inter.typ, missingMethod: m.init()})
}

        这里主要的逻辑是:

  • 更具interfacetype和_type,尝试从itabTable中获取itab
  • 如果itabTable没有找到itab,则新创建一个itab并将其缓存到itabTable中
  • 判断该类型是否实现了接口的所有方法(m.fun[0]!=0)
  • 如果该类型没有实现接口的所有方法,则根据canfail判断是否panic,canfail为false则会panic。比如类型断言时,如果不接受第二个返回值,则断言失败会panic

        首次调用getitab方法获取时,哈希表中是没有对应数据的。此时不仅要创建itab结构,还要对其涉及到的接口和类型的方法集进行判断,初始化等。相关代码在init中。

func (m *itab) init() string {
    inter := m.inter
    typ := m._type
    x := typ.uncommon()

    // 接口定义的方法数量
    ni := len(inter.mhdr)
  // 实际类型的方法数量
    nt := int(x.mcount)
  // 实际类型的方法数组
    xmhdr := (*[1 << 16]method)(add(unsafe.Pointer(x), uintptr(x.moff)))[:nt:nt]
    j := 0
  // 保存接口的第i个方法对应的实际类型的方法的地址
    methods := (*[1 << 16]unsafe.Pointer)(unsafe.Pointer(&m.fun[0]))[:ni:ni]
    var fun0 unsafe.Pointer
imethods:
  // 遍历接口方法列表
    for k := 0; k < ni; k++ {
    // 接口的方法
        i := &inter.mhdr[k]
    // 接口的方法类型
        itype := inter.typ.typeOff(i.ityp)
    // 接口的方法名称
        name := inter.typ.nameOff(i.name)
    // 接口的方法名
        iname := name.name()
    // 接口的包路径
        ipkg := name.pkgPath()
        if ipkg == "" {
            ipkg = inter.pkgpath.name()
        }
    // 根据接口方法查找实际类型的方法
        for ; j < nt; j++ {
      // 实际类型的方法
            t := &xmhdr[j]
      // 实际类型的方法名
            tname := typ.nameOff(t.name)
      // 比较接口的方法名和实际类型的方法是否一致,包括名称和类型
            if typ.typeOff(t.mtyp) == itype && tname.name() == iname {
                pkgPath := tname.pkgPath()
                if pkgPath == "" {
                    pkgPath = typ.nameOff(x.pkgpath).name()
                }
        // 如果是导出方法或在同一个包,则将将方法保存到 itab 中
                if tname.isExported() || pkgPath == ipkg {
                    if m != nil {
            // 实际类型的方法指针,通过该指针可以调用实际类型的方法
                        ifn := typ.textOff(t.ifn)
                        if k == 0 {
                            fun0 = ifn // we'll set m.fun[0] at the end
                        } else {
                            methods[k] = ifn
                        }
                    }
                    continue imethods
                }
            }
        }
        // 该类型没有实现接口
    // 如果每个接口方法都被实现了,则每次都会走到 continue 的逻辑,不会将 fuc[0] 置为 0
        m.fun[0] = 0
        return iname
    }
    m.fun[0] = uintptr(fun0)
    return ""
}

        主要逻辑是,依次遍历接口的所有方法,并在实际类型的接口列表中查找对应的实现,只要有一个接口方法没有被实现,则将itab的fun[0]置为0,表示该类型没有实现该接口。

  • itabadd

        getitab方法如果没有找到itab,会新建一个itab并调用itabAdd方法将其缓存到itabTable中。

func itabAdd(m *itab) {
    ...
    t := itabTable
  // 容量超过 75% 时会触发扩容
    if t.count >= 3*(t.size/4) { // 75% load factor
        // 扩容为原哈希表的 2 倍大小
        t2 := (*itabTableType)(mallocgc((2+2*t.size)*goarch.PtrSize, nil, true))
        t2.size = t.size * 2

        // 将原哈希表的元素复制到新哈希表
    // 复制过程中,其他的线程可能会尝试从原哈希表中获取 itab,但找不到。此时会尝试获取锁(会阻塞)后再次获取。
        iterate_itabs(t2.add)-】
        if t2.count != t.count {
            throw("mismatched count during itab table copy")
        }
        // 使用原子操作将 itabTable 的引用指向新扩容的内存
        atomicstorep(unsafe.Pointer(&itabTable), unsafe.Pointer(t2))
        // Adopt the new table as our own.
        t = itabTable
        // Note: the old table can be GC'ed here.
    }
  // 将新的 itab 缓存到 itabTable 中
    t.add(m)
}

        3.3 接口赋值

        将某一具体类型赋值给接口类型时,本质其实时如何填充eface和iface结构体。

        对eface结构体来说,由于只有_type和data字段,因此只需要进行字段赋值即可。

        对于iface结构体来说,需要通过itab判断类型值是否实现了接口的所有方法(itab可能不存在,会走一遍getitab的流程),然后初始化iface结构的tab和data字段。

        底层会调用runtime.convTXXX转换为iface或eface的data字段。

func convT(t *_type, v unsafe.Pointer) unsafe.Pointer {
    ...
  // 分配`_type`所需的内存
    x := mallocgc(t.size, t, true)
  // 将v的值复制到刚分配的内存
    typedmemmove(t, x, v)
    return x
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/537769.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

利用免费AI开源引擎:实现图像识别技术在多主体检测中的应用|识别万物|本地化部署

在当今快速发展的图像处理领域&#xff0c;图像主体检测技术已成为提升图像分析效率和精度的关键工具。该技术能够自动识别和定位图像中的一个或多个主要对象&#xff0c;并提供其具体的位置坐标和分类标签。这不仅为图像编辑和优化提供了便利&#xff0c;也为后续的图像识别任…

C-开发 visual Studio扩展插件介绍-格式化插件Xaml Styler、CSharpier介绍(扩展插件安装方法)

C#开发 visual Studio扩展插件介绍 扩展插件安装方法Xaml StylerCSharpier 提高C#开发效率常用的插件 扩展插件安装方法 菜单栏点击“扩展”→“管理扩展”。 打开扩展页面 右上角搜索需要安装的插件&#xff0c;然后点击下载 安装完成后&#xff0c;根据提示关闭VS进行安…

selenium绕过网站检测的方法

使用selenium打开如下网站&#xff0c;进行检测&#xff0c;代码如下&#xff1a; from selenium import webdriver import timedriver webdriver.Chrome() driver.get(https://bot.sannysoft.com/) time.sleep(60)发现webdriver被检测到了 在这里可使用一个selenium提供的插…

【MATLAB源码-第7期】基于matlab的8PSK的实际误码率BER和理论误码率BER对比仿真。

1、算法描述 8PSK (8 Phase Shift Keying 8移相键控) 是一种相位调制算法。相位调制&#xff08;调相&#xff09;是频率调制&#xff08;调频&#xff09;的一种演变&#xff0c;载波的相位被调整用于把数字信息的比特编码到每一次相位改变&#xff08;相移&#xff09;。&quo…

是时候将 DevOps 可见性扩展到网络边缘了

尽管部署前运行了大量测试&#xff0c;但在部署应用程序后&#xff0c;性能问题经常让 DevOps 团队感到困惑。经过进一步调查&#xff0c;最常被忽视的问题是应用程序本身的分布式特性。从多个位置访问应用程序的最终用户永远不会拥有相同水平的互联网服务&#xff0c;因此在纽…

让大模型落地有“技”可循

“2018年&#xff0c;随着Transformer预训练模型的兴起&#xff0c;自然语言处理&#xff08;NLP&#xff09;学术圈中形成了一个主流观点——NLP领域的不同技术方向&#xff0c;如文本分类、文本匹配、序列标注等&#xff0c;最终都会被归结到文本生成这一核心任务之下。”这是…

【大语言模型】基础:如何处理文章,向量化与BoW

词袋模型&#xff08;BoW&#xff09;是自然语言处理&#xff08;NLP&#xff09;和机器学习中一种简单而广泛使用的文本表示方法。它将文本文档转换为数值特征向量&#xff0c;使得可以对文本数据执行数学和统计操作。词袋模型将文本视为无序的单词集合&#xff08;或“袋”&a…

【电控笔记0】稳定度判断

简要概括 现控:原理虚轴,稳定度越高 自控:相位裕度PM 增益裕度GM 开环传函 不稳定条件判断

Proteus 8 的使用记录

创建仿真文件 新建文件&#xff1a;默认下一步&#xff0c;至完成创建。 功能选择如图&#xff1a; 放置器件 常用元器件名称 keywords 常用51单片机 AT89C52 晶振 CRYSTAL 电阻 RES 排阻 RESPACK-8 瓷片电容 CAP 电解电容 CAP-ELEC 单刀单掷开关 S…

【教学类-52-01】20240411动物数独(4宫格)

作品展示 背景需求&#xff1a; 一、下载图片 PS修图&#xff08;图片长宽一样&#xff0c;把动物图片上下拉长&#xff09; 二、数独结构分析&#xff1a; 1、这是一个四宫格的数独题&#xff0c; 2、将1234换成了四种小动物图片。 于是我去找到原来做过的一个代码&#xf…

day05-java面向对象(上)

5.1 面向对象编程 5.1.1 类和对象 1、什么是类 类是一类具有相同特性的事物的抽象描述&#xff0c;是一组相关属性和行为的集合。 属性&#xff1a;就是该事物的状态信息。 行为&#xff1a;就是在你这个程序中&#xff0c;该状态信息要做什么操作&#xff0c;或者基于事物…

web安全-SSH私钥泄露

发现主机 netdiscover -r 192.168.164.0 扫描端口 看到开放80和31337端口都为http服务 浏览器访问测试 查看80端口和31337端口网页和源代码并无发现有用信息 目录扫描 扫描出80端口并无有用信息 扫描31337端口 发现敏感文件robots.txt和目录.ssh 访问敏感文件和目录 /.ss…

pugixml C++ 开发者处理 XML 数据的理想选择之一

pugixml 是一个广受好评的 C XML 解析库&#xff0c;其相对优势包括但不限于以下几个方面&#xff1a; pugixml 以其高效、易用、全面的功能和良好的跨平台能力成为 C 开发者处理 XML 数据的理想选择之一。 链接&#xff1a; 使用Pugixml库&#xff0c;轻松处理XML文件-CSDN…

vue 原理【详解】MVVM、响应式、模板编译、虚拟节点 vDom、diff 算法

vue 的设计模式 —— MVVM M —— Model 模型&#xff0c;即数据V —— View 视图&#xff0c;即DOM渲染VM —— ViewModel 视图模型&#xff0c;用于实现Model和View的通信&#xff0c;即数据改变驱动视图渲染&#xff0c;监听视图事件修改数据 初次渲染 将模板编译为 render …

Prometheus报错,查不到数据

Warning: Error fetching server time: Detected 28799.947999954224 seconds time difference between your browser and the server. Prometheus relies on accurate time and time drift might cause unexpected query results. 1.这是因为服务器和本地时间不同步导致的 查…

抖店怎么回复客户消息才能减少差评?分享几个超级实用的话术!

哈喽~我是电商月月 新手入驻抖音小店出单后&#xff0c;或多或少都会遇到差评现象 差评私信不解决&#xff0c;顾客不满意&#xff0c;店铺的体验分下降&#xff0c;差评也能被所有的顾客看见 那之后的顾客就会觉得店铺不可靠&#xff0c;那新手如何避免这一现象呢 今天我就…

SLF4J对lombok类型的对象调用toString()失败--StackOverflowError

PackingDemand.class StatusHistory.class 造成该问题的原因是&#xff1a;PackingDemand与StatusHistory之间的双向引用。这些类中生成的两个toString()方法都会无休止地相互调用导致出现java.lang.StackOverflowError。 解决方法&#xff1a; 1.对于使用ToString.Exclude生…

Redis学习从入门到掌握(基础篇)

文章目录 一、初识Redis1.认识 Redis2.Redis常见命令&#xff08;1&#xff09;Redis 数据结构介绍&#xff08;2&#xff09;Redis 通用命令&#xff08;3&#xff09;String 类型&#xff08;4&#xff09;String 类型的常见命令&#xff08;5&#xff09;Hash 类型&#xff…

练习6 beach beauty

文章目录 图片展示 图片展示

说说我理解的数据库中的Schema吧

一、SQL标准对schema如何定义&#xff1f; ISO/IEC 9075-1 SQL标准中将schema定义为描述符的持久命名集合&#xff08;a persistent, named collection of descriptors&#xff09;。 大部分的网上资料定义Schema如下&#xff1a; schema是用来组织和管理数据的一种方式。它…
最新文章