go中interface转换成原来的类型

首先了解下interface

什么是interface?

首先 interface 是一种类型,从它的定义可以看出来用了 type 关键字,更准确的说 interface 是一种具有一组方法的类型,这些方法定义了 interface 的行为。

type I interface {
Get() int
}

interface是一组method的集合,是duck-type programming的一种体现(不关心属性(数据),只关心行为(方法))。我们可以自己定义interface类型的struct,并提供方法。

type MyInterface interface{
Print()
} func TestFunc(x MyInterface) {}
type MyStruct struct {}
func (me MyStruct) Print() {} func main() {
var me MyStruct
TestFunc(me)
}

go 允许不带任何方法的 interface ,这种类型的 interfaceempty interface

如果一个类型实现了一个 interface 中所有方法,必须是所有的方法,我们说类型实现了该 interface,所以所有类型都实现了 empty interface,因为任何一种类型至少实现了 0 个方法。go 没有显式的关键字用来实现 interface,只需要实现 interface 包含的方法即可。

interface还可以作为返回值使用。

如何判断interface变量存储的是哪种类型

日常中使用interface,有时候需要判断原来是什么类型的值转成了interface。一般有以下几种方式:

fmt
import "fmt"
func main() {
v := "hello world"
fmt.Println(typeof(v))
}
func typeof(v interface{}) string {
return fmt.Sprintf("%T", v)
}
反射
import (
"reflect"
"fmt"
)
func main() {
v := "hello world"
fmt.Println(typeof(v))
}
func typeof(v interface{}) string {
return reflect.TypeOf(v).String()
}
断言

Go语言里面有一个语法,可以直接判断是否是该类型的变量: value, ok = element.(T),这里value就是变量的值,ok是一个bool类型,elementinterface变量,T是断言的类型。

如果element里面确实存储了T类型的数值,那么ok返回true,否则返回false

让我们通过一个例子来更加深入的理解。

value, ok := v.(string)

if ok {
return value
}

类型不确定可以配合switch:

func main() {
v := "hello world"
fmt.Println(typeof(v))
}
func typeof(v interface{}) string {
switch t := v.(type) {
case int:
return "int"
case float64:
return "float64"
//... etc
default:
_ = t
return "unknown"
}
}

对于fmt也是用了反射的,同时里面也用到了断言:

func (p *pp) printArg(arg interface{}, verb rune) {
p.arg = arg
p.value = reflect.Value{} if arg == nil {
switch verb {
case 'T', 'v':
p.fmt.padString(nilAngleString)
default:
p.badVerb(verb)
}
return
} // Special processing considerations.
// %T (the value's type) and %p (its address) are special; we always do them first.
switch verb {
case 'T':
p.fmt.fmtS(reflect.TypeOf(arg).String())
return
case 'p':
p.fmtPointer(reflect.ValueOf(arg), 'p')
return
} // Some types can be done without reflection.
switch f := arg.(type) {
case bool:
p.fmtBool(f, verb)
case float32:
p.fmtFloat(float64(f), 32, verb)
case float64:
p.fmtFloat(f, 64, verb)
case complex64:
p.fmtComplex(complex128(f), 64, verb)
case complex128:
p.fmtComplex(f, 128, verb)
case int:
p.fmtInteger(uint64(f), signed, verb)
case int8:
p.fmtInteger(uint64(f), signed, verb)
case int16:
p.fmtInteger(uint64(f), signed, verb)
case int32:
p.fmtInteger(uint64(f), signed, verb)
case int64:
p.fmtInteger(uint64(f), signed, verb)
case uint:
p.fmtInteger(uint64(f), unsigned, verb)
case uint8:
p.fmtInteger(uint64(f), unsigned, verb)
case uint16:
p.fmtInteger(uint64(f), unsigned, verb)
case uint32:
p.fmtInteger(uint64(f), unsigned, verb)
case uint64:
p.fmtInteger(f, unsigned, verb)
case uintptr:
p.fmtInteger(uint64(f), unsigned, verb)
case string:
p.fmtString(f, verb)
case []byte:
p.fmtBytes(f, verb, "[]byte")
case reflect.Value:
// Handle extractable values with special methods
// since printValue does not handle them at depth 0.
if f.IsValid() && f.CanInterface() {
p.arg = f.Interface()
if p.handleMethods(verb) {
return
}
}
p.printValue(f, verb, 0)
default:
// If the type is not simple, it might have methods.
if !p.handleMethods(verb) {
// Need to use reflection, since the type had no
// interface methods that could be used for formatting.
p.printValue(reflect.ValueOf(f), verb, 0)
}
}
}

下面来简单探究下反射是如何判断interface

// TypeOf returns the reflection Type that represents the dynamic type of i.
// If i is a nil interface value, TypeOf returns nil.
func TypeOf(i interface{}) Type {
eface := *(*emptyInterface)(unsafe.Pointer(&i))
return toType(eface.typ)
}

eface := *(*emptyInterface)(unsafe.Pointer(&i))用到了一个emptyInterface,我们来看下这个结构的信息:

// emptyInterface is the header for an interface{} value.
type emptyInterface struct {
typ *rtype
word unsafe.Pointer
}

其中typ指向一个rtype实体, 它表示interface的类型以及赋给这个interface的实体类型。word则指向interface具体的值,一般而言是一个指向堆内存的指针。

TypeOf看到的是空接口interface{},它将变量的地址转换为空接口,然后将得到的rtype转为Type接口返回。需要注意,当调用reflect.TypeOf的之前,已经发生了一次隐式的类型转换,即将具体类型的向空接口转换。这个过程比较简单,只要拷贝typ *rtypeword unsafe.Pointer就可以了。

来看下interface的底层源码

我的go版本是go version go1.13.7

ifaceeface都是Go中描述接口的底层结构体,区别在于iface描述的接口包含方法,而eface则是不包含任何方法的空接口:interface{}

eface

代码在runtime/runtime2.go:

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

eface有两个字段,_type指向对象的类型信息,data数据指针。指针指向的数据地址,一般是在堆上的。

我们来看下_type

// src/rumtime/runtime2.go
type _type struct {
size uintptr // 类型的大小
ptrdata uintptr // size of memory prefix holding all pointers
hash uint32 // 类型的Hash值
tflag tflag // 类型的Tags
align uint8 // 结构体内对齐
fieldalign uint8 // 结构体作为field时的对齐
kind uint8 // 类型编号 定义于runtime/typekind.go
alg *typeAlg // 类型元方法 存储hash和equal两个操作。map key便使用key的_type.alg.hash(k)获取hash值
gcdata *byte // GC相关信息
str nameOff // 类型名字的偏移
ptrToThis typeOff
}

_typego中类型的公共描述,里面包含GC,反射等需要的细节,它决定data应该如何解释和操作。对于不同的数据类型它的描述信息是不一样的,在_type的基础之上配合一些额外的描述信息,来进行区分。

// src/runtime/type.go
// ptrType represents a pointer type.
type ptrType struct {
typ _type // 指针类型
elem *_type // 指针所指向的元素类型
}
type chantype struct {
typ _type // channel类型
elem *_type // channel元素类型
dir uintptr
}
type maptype struct {
typ _type
key *_type
elem *_type
bucket *_type // internal type representing a hash bucket
hmap *_type // internal type representing a hmap
keysize uint8 // size of key slot
indirectkey bool // store ptr to key instead of key itself
valuesize uint8 // size of value slot
indirectvalue bool // store ptr to value instead of value itself
bucketsize uint16 // size of bucket
reflexivekey bool // true if k==k for all keys
needkeyupdate bool // true if we need to update key on an overwrite
}

这些类型信息的第一个字段都是_type(类型本身的信息),接下来是一堆类型需要的其它详细信息(如子类型信息),这样在进行类型相关操作时,可通过一个字(typ *_type)即可表述所有类型,然后再通过_type.kind可解析出其具体类型,最后通过地址转换即可得到类型完整的”_type树”,参考reflect.Type.Elem()函数:

// reflect/type.go
// reflect.rtype结构体定义和runtime._type一致 type.kind定义也一致(为了分包而重复定义)
// Elem()获取rtype中的元素类型,只针对复合类型(Array, Chan, Map, Ptr, Slice)有效
func (t *rtype) Elem() Type {
switch t.Kind() {
case Array:
tt := (*arrayType)(unsafe.Pointer(t))
return toType(tt.elem)
case Chan:
tt := (*chanType)(unsafe.Pointer(t))
return toType(tt.elem)
case Map:
// 对Map来讲,Elem()得到的是其Value类型
// 可通过rtype.Key()得到Key类型
tt := (*mapType)(unsafe.Pointer(t))
return toType(tt.elem)
case Ptr:
tt := (*ptrType)(unsafe.Pointer(t))
return toType(tt.elem)
case Slice:
tt := (*sliceType)(unsafe.Pointer(t))
return toType(tt.elem)
}
panic("reflect: Elem of invalid type")
}

iface

表示的是非空的接口:

type iface struct {
tab *itab
data unsafe.Pointer
} // layout of Itab known to compilers
// allocated in non-garbage-collected memory
// Needs to be in sync with
// ../cmd/compile/internal/gc/reflect.go:/^func.dumptypestructs.
type itab struct {
inter *interfacetype // 接口定义的类型信息
_type *_type // 接口实际指向值的类型信息
hash uint32 // copy of _type.hash. Used for type switches.
_ [4]byte
fun [1]uintptr // 接口方法实现列表,即函数地址列表,按字典序排序 variable sized
}
// runtime/type.go
// 非空接口类型,接口定义,包路径等。
type interfacetype struct {
typ _type
pkgpath name
mhdr []imethod // 接口方法声明列表,按字典序排序
} // 接口的方法声明
type imethod struct {
name nameOff // 方法名
ityp typeOff // 描述方法参数返回值等细节
}

iface同样也是有两个指针,tab指向一个itab实体, 它表示接口的类型以及赋给这个接口的实体类型。data则指向接口具体的值,一般而言是一个指向堆内存的指针。

fun表示interfacemethod的具体实现。比如interfacetype包含了两个method分别是AB。但是有一点很奇怪,这个fun是长度为1的uintptr数组,那么是怎么表示多个的呢?

其实上面源码的注释已经能给到我们答案了,variable sized,这是个是可变大小的。go中的uintptr一般用来存放指针的值,那这里对应的就是函数指针的值(也就是函数的调用地址)。如果有更多的方法,在它之后的内存空间里继续存储。也就是在fun[0]后面一次写入其他method对应的函数指针。

接口的类型转换是怎么实现的呢?

举个例子:

type coder interface {
code()
run()
} type runner interface {
run()
} type Gopher struct {
language string
} func (g Gopher) code() {
return
} func (g Gopher) run() {
return
} func main() {
var c coder = Gopher{} var r runner
r = c
fmt.Println(c, r)
}

定义了两个 interface: coderrunner。定义了一个实体类型 Gopher,类型 Gopher 实现了两个方法,分别是 run()code()main 函数里定义了一个接口变量 c,绑定了一个 Gopher 对象,之后将 c 赋值给另外一个接口变量 r 。赋值成功的原因是 c 中包含 run() 方法。这样,两个接口变量完成了转换。

上面的转换调用了下面的函数实现的

func convI2I(inter *interfacetype, i iface) (r iface) {
tab := i.tab
if tab == nil {
return
}
if tab.inter == inter {
r.tab = tab
r.data = i.data
return
}
r.tab = getitab(inter, tab._type, false)
r.data = i.data
return
}

关于conv的函数定义,其中E代表eface,I代表iface,T代表编译器已知类型,即静态类型。

inter表示转换之后的接口类型,i表示转换之前的实体类型接口,r表示转换之后的实体类型接口。

这个函数先做了判断,如果两个转换之前和转换之后的接口类型是一样的,就直接把转换之前的接口信息赋值给r就可以了。如果不一样,就调用getitab

func getitab(inter *interfacetype, typ *_type, canfail bool) *itab {
if len(inter.mhdr) == 0 {
throw("internal error - misuse of itab")
} // easy case
if typ.tflag&tflagUncommon == 0 {
if canfail {
return nil
}
name := inter.typ.nameOff(inter.mhdr[0].name)
panic(&TypeAssertionError{nil, typ, &inter.typ, name.name()})
} var m *itab // First, look in the existing table to see if we can find the itab we need.
// This is by far the most common case, so do it without locks.
// Use atomic to ensure we see any previous writes done by the thread
// that updates the itabTable field (with atomic.Storep in itabAdd).
t := (*itabTableType)(atomic.Loadp(unsafe.Pointer(&itabTable)))
if m = t.find(inter, typ); m != nil {
goto finish
} // Not found. Grab the lock and try again.
lock(&itabLock)
if m = itabTable.find(inter, typ); m != nil {
unlock(&itabLock)
goto finish
} // Entry doesn't exist yet. Make a new entry & add it.
m = (*itab)(persistentalloc(unsafe.Sizeof(itab{})+uintptr(len(inter.mhdr)-1)*sys.PtrSize, 0, &memstats.other_sys))
m.inter = inter
m._type = typ
m.init()
itabAdd(m)
unlock(&itabLock)
finish:
if m.fun[0] != 0 {
return m
}
if canfail {
return nil
}
// this can only happen if the conversion
// was already done once using the , ok form
// and we have a cached negative result.
// The cached result doesn't record which
// interface function was missing, so initialize
// the itab again to get the missing function name.
panic(&TypeAssertionError{concrete: typ, asserted: &inter.typ, missingMethod: m.init()})
}

简单总结一下:getitab 函数会根据 interfacetype_type 去全局的 itab 哈希表中查找,如果能找到,则直接返回;否则,会根据给定的 interfacetype_type 新生成一个 itab,并插入到 itab 哈希表,这样下一次就可以直接拿到 itab

第一次去查询的时候如果查找到,直接返回

if m = t.find(inter, typ); m != nil {
goto finish
}

如果在hash表中没有找到,这时候锁住itabLock,然后去重新写入itab到哈希表,当写入之后,上游的查询拿到值了,解除锁的阻塞,然后返回。

if m = itabTable.find(inter, typ); m != nil {
unlock(&itabLock)
goto finish
}

再来看一下 itabAdd 函数的代码:

// itabAdd adds the given itab to the itab hash table.
// itabLock must be held.
func itabAdd(m *itab) {
// Bugs can lead to calling this while mallocing is set,
// typically because this is called while panicing.
// Crash reliably, rather than only when we need to grow
// the hash table.
if getg().m.mallocing != 0 {
throw("malloc deadlock")
} t := itabTable
if t.count >= 3*(t.size/4) { // 75% load factor
// Grow hash table.
// t2 = new(itabTableType) + some additional entries
// We lie and tell malloc we want pointer-free memory because
// all the pointed-to values are not in the heap.
t2 := (*itabTableType)(mallocgc((2+2*t.size)*sys.PtrSize, nil, true))
t2.size = t.size * 2 // Copy over entries.
// Note: while copying, other threads may look for an itab and
// fail to find it. That's ok, they will then try to get the itab lock
// and as a consequence wait until this copying is complete.
iterate_itabs(t2.add)
if t2.count != t.count {
throw("mismatched count during itab table copy")
}
// Publish new hash table. Use an atomic write: see comment in getitab.
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.
}
t.add(m)
}

最后总结下:

  • 1、具体类型转空接口时,_type 字段直接复制源类型的 _type;调用 mallocgc 获得一块新内存,把值复制进去,data 再指向这块新内存。
  • 2、具体类型转非空接口时,入参 tab 是编译器在编译阶段预先生成好的,新接口 tab 字段直接指向入参 tab 指向的 itab;调用 mallocgc 获得一块新内存,把值复制进去,data 再指向这块新内存。
  • 3、而对于接口转接口,itab 调用 getitab 函数获取。只用生成一次,之后直接从 hash 表中获取。

接口的动态类型和动态值

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

iface我们可以看到,是有一个tab接口指针,指向数据类型,data数据指针,指向具体的数据。他们也被称为动态类型动态值

因为两个都是指针,所以默认值都是nil。所以当两者都是nil的时候这个接口值才是nil,也就是接口值 == nil

func main() {
var f interface{}
fmt.Println("+++动态类型和动态值都是nil+++")
fmt.Println(f == nil)
fmt.Printf("f: %T, %v\n", f, f) var g *string
f = g
fmt.Println("+++类型为 *string+++")
fmt.Println(f == nil)
fmt.Printf("f: %T, %v\n", f, f)
}

打印下输出:

+++动态类型和动态值都是nil+++
true
f: <nil>, <nil>
+++类型为 *string+++
false
f: *string, <nil>

interface如何支持泛型

严格来说,在 Golang 中并不支持泛型编程。在 C++ 等高级语言中使用泛型编程非常的简单,所以泛型编程一直是 Golang 诟病最多的地方。但是使用 interface 我们可以实现“泛型编程”,为什么?因为 interface 是一种抽象类型,任何具体类型(int, string)和抽象类型(user defined)都可以封装成 interface。以标准库的 sort 为例。

package sort

// A type, typically a collection, that satisfies sort.Interface can be
// sorted by the routines in this package. The methods require that the
// elements of the collection be enumerated by an integer index.
type Interface interface {
// Len is the number of elements in the collection.
Len() int
// Less reports whether the element with
// index i should sort before the element with index j.
Less(i, j int) bool
// Swap swaps the elements with indexes i and j.
Swap(i, j int)
} ... // Sort sorts data.
// It makes one call to data.Len to determine n, and O(n*log(n)) calls to
// data.Less and data.Swap. The sort is not guaranteed to be stable.
func Sort(data Interface) {
// Switch to heapsort if depth of 2*ceil(lg(n+1)) is reached.
n := data.Len()
maxDepth := 0
for i := n; i > 0; i >>= 1 {
maxDepth++
}
maxDepth *= 2
quickSort(data, 0, n, maxDepth)
}

Sort 函数的形参是一个 interface,包含了三个方法:Len(),Less(i,j int),Swap(i, j int)。使用的时候不管数组的元素类型是什么类型(int, float, string…),只要我们实现了这三个方法就可以使用 Sort 函数,这样就实现了“泛型编程”。有一点比较麻烦的是,我们需要自己封装一下。下面是一个例子。

type Person struct {
Name string
Age int
} func (p Person) String() string {
return fmt.Sprintf("%s: %d", p.Name, p.Age)
} // ByAge implements sort.Interface for []Person based on
// the Age field.
type ByAge []Person //自定义 func (a ByAge) Len() int { return len(a) }
func (a ByAge) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age } func main() {
people := []Person{
{"Bob", 31},
{"John", 42},
{"Michael", 17},
{"Jenny", 26},
} fmt.Println(people)
sort.Sort(ByAge(people))
fmt.Println(people)
}

具体一点来说,也就是如果是在实现一个服务时,对于不同场景,可以将其共同特征抽象出来,在一个interface中声明,然后给不同的场景定义其特定的struct,上层的逻辑可以通过传入interface来执行,特化则通过struct实现对应的方法,从而达到一定程度的泛型。

参考

【理解 Go interface 的 5 个关键点】https://sanyuesha.com/2017/07/22/how-to-understand-go-interface/

【深入理解 Go Interface】https://zhuanlan.zhihu.com/p/32926119

【GO如何支持泛型】https://zhuanlan.zhihu.com/p/74525591

【Golang面向对象编程】https://code.tutsplus.com/zh-hans/tutorials/lets-go-object-oriented-programming-in-golang--cms-26540

【深度解密Go语言之关于 interface 的10个问题】https://www.cnblogs.com/qcrao-2018/p/10766091.html

【golang如何获取变量的类型:反射,类型断言】https://ieevee.com/tech/2017/07/29/go-type.html

【Go接口详解】https://zhuanlan.zhihu.com/p/27055513

go中的类型转换成interface之后如何复原的更多相关文章

  1. Java中Clob类型转换成String类型的问题

    1.问题: 项目中使用druid+达梦数据库(基本类似Oracle),查出的Clob类型数据在运行时为ClobProxyImpl对象而不是内容,不能转为字符串 2.原代码: map为达梦数据库或Ora ...

  2. jquery中字符串类型转换成整形的方法

    jQuery有一个自带的函数为parseInt():这个函数可以把字符型的数字转换成整形例如: parseInt("1234"); //返回1234 parseInt(" ...

  3. java中string类型转换成map

    背景:有时候string类型的数据取出来是个很标准的key.value形式,通过Gson的可以直接转成map 使用方式: Gson gson = new Gson(); Map<String, ...

  4. java中char类型转换成int类型的两种方法

    方法一: char ch = '9'; if (Character.isDigit(ch)){ // 判断是否是数字 int num = Integer.parseInt(String.valueOf ...

  5. NX二次开发-NXOpen中Point3d类型转换成point类型

    NX9+VS2012 #include <NXOpen/NXObject.hxx> #include <NXOpen/Part.hxx> #include <NXOpen ...

  6. golang中float类型转换成int类型

    package main import ( "fmt" "strconv" ) func f2i(f float64) int { i, _ := strcon ...

  7. sqlserver搜索中怎么把varchar类型转换成numeric类型

    sqlserver搜索中怎么把varchar类型转换成numeric类型 可以用cast来转换 如:列名叫grade,表名为A select cast(grade as numeric(y,x)) f ...

  8. c#中的里氏转换和Java中强制类型转换在多态中的应用

    在c#中: 注意: 子类并没有继承父类的构造函数,而是会默认调用父类那个无参数的构造函数. 如果一个子类继承了一个父类,那么这个子类除了可以使用自己的成员外,还可以使用从父类那里继承过来的成员.但是父 ...

  9. JavaScript中数据类型转换总结

    JavaScript中数据类型转换总结 在js中,数据类型转换分为显式数据类型转换和隐式数据类型转换. 1, 显式数据类型转换 a:转数字: 1)Number转换: 代码: var a = " ...

  10. java中强制类型转换

    在Java中强制类型转换分为基本数据类型和引用数据类型两种,这里我们讨论的后者,也就是引用数据类型的强制类型转换. 在Java中由于继承和向上转型,子类可以非常自然地转换成父类,但是父类转换成子类则需 ...

随机推荐

  1. Kubernetes(K8S) 镜像拉取策略 imagePullPolicy

    镜像仓库,镜像已更新,版本没更新, K8S 拉取后,还是早的服务,原因:imagePullPolicy 镜像拉取策略 默认为本地有了就不拉取,需要修改 [root@k8smaster ~]# kube ...

  2. JMeter 源码解读 - HashTree

    背景: 在 JMeter 中,HashTree 是一种用于组织和管理测试计划元素的数据结构.它是一个基于 LinkedHashMap 的特殊实现,提供了一种层次结构的方式来存储和表示测试计划的各个组件 ...

  3. leaflet marker 旋转

    leaflet.markerRotation.js 代码(这段代码是从插件 leaflet.polylineDecorator.js 中复制的): // leaflet 实现 marker 旋转 (f ...

  4. Mac | 解决 MacOS 配置 Maven 出现的 Java_Home Error

    1. 错误信息 2. 解决方案 2.1 对于Windows系统下解决方案 https://blog.csdn.net/frankarmstrong/article/details/69945774,在 ...

  5. Educational Codeforces Round 104 (Rated for Div. 2) A-E 个人题解

    比赛链接 1487A. Arena n 个 Hero,分别有 \(a_i\) 的初始等级.每次两个 Hero 战斗时:等级相同无影响,否则等级高的英雄等级+1.直到某个英雄等级到了 \(100^{50 ...

  6. Codeforces Round #700 (Div. 2) A ~ D1个人题解

    Codeforces Round #700 (Div. 2) 比赛链接: Click Here 1480A. Yet Another String Game 因为Alice是要追求小,Bob追求大值, ...

  7. 图解 Promise 实现原理(四)—— Promise 静态方法实现

    本文首发于 vivo互联网技术 微信公众号 链接:https://mp.weixin.qq.com/s/Lp_5BXdpm7G29Z7zT_S-bQ作者:Morrain 了用法,原生提供了Promis ...

  8. iview+vue 加载进度条

    效果:浏览器最上方出现一个进度条. main.js import Vue from 'vue' import ViewUI from 'view-design'; import router from ...

  9. sprint-boot 存储图片的base64

    需求:将前端上传的图片转换成base64码发送到后端存储到数据库中(oracle或者mysql) 问题:当图片大小比较大(大概是超过1M)后端接收到的数据就会有错误. 解决方法:  sprint-bo ...

  10. scroll-view横向滚动的问题

    最近在做一个小程序的项目,在写demo的时候,需要用到scroll-view来实现横向滚动的效果: 按照官方文档来写简直坑到家了,正确的写法如下: <scroll-view scroll-x=& ...