了解go语言中的内存模型 go语言内存占用

golang获取到string和直接赋值strimg不一样

1、 string的定义

从网站建设到定制行业解决方案,为提供成都网站设计、网站建设服务体系,各种行业企业客户提供网站建设解决方案,助力业务快速发展。创新互联公司将不断加快创新步伐,提供优质的建站服务。

Golang中的string的定义在reflect包下的value.go中,定义如下:

StringHeader 是字符串的运行时表示,其中包含了两个字段,分别是指向数据数组的指针和数组的长度。

// StringHeader is the runtime representation of a string.

// It cannot be used safely or portably and its representation may

// change in a later release.

// Moreover, the Data field is not sufficient to guarantee the data

// it references will not be garbage collected, so programs must keep

// a separate, correctly typed pointer to the underlying data.

type StringHeader struct {

Data uintptr

Len int

}

1

2

3

4

5

6

7

8

9

10

1

2

3

4

5

6

7

8

9

10

2、string不可变

Golang中的字符串是不可变的,不能通过索引下标的方式修改字符串中的数据:

在这里插入图片描述

运行代码,可以看到编译器报错,string是不可变的

在这里插入图片描述

但是能不能进行一些骚操作来改变元素的值呢?

package main

import (

"fmt"

"reflect"

"unsafe"

)

func main() {

a := "hello,world"

b := a[6:]

bptr := (*reflect.StringHeader) (unsafe.Pointer(b))

fmt.Println(a)

fmt.Println(b)

*(*byte)(unsafe.Pointer(bptr.Data)) = '.'

fmt.Println(a)

fmt.Println(b)

}

// 运行结果

hello,world

world

unexpected fault address 0x49d7e3

fatal error: fault

[signal 0xc0000005 code=0x1 addr=0x49d7e3 pc=0x4779fa]

goroutine 1 [running]:

runtime.throw(0x49c948, 0x5)

C:/Program Files/Go/src/runtime/panic.go:1117 +0x79 fp=0xc0000dbe90 sp=0xc0000dbe60 pc=0x405fd9

runtime.sigpanic()

C:/Program Files/Go/src/runtime/signal_windows.go:245 +0x2d6 fp=0xc0000dbee8 sp=0xc0000dbe90 pc=0x4189f6

main.main()

F:/go_workspace/src/code/string_test/main.go:20 +0x13a fp=0xc0000dbf88 sp=0xc0000dbee8 pc=0x4779fa

runtime.main()

C:/Program Files/Go/src/runtime/proc.go:225 +0x256 fp=0xc0000dbfe0 sp=0xc0000dbf88 pc=0x4087f6

runtime.goexit()

C:/Program Files/Go/src/runtime/asm_amd64.s:1371 +0x1 fp=0xc0000dbfe8 sp=0xc0000dbfe0 pc=0x435da1

Process finished with the exit code 2

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

在上面的代码中,因为在go语言中不能进行指针的加减运算,因此取切片,让b的Data指针指向’,'所在的位置。然后把"hello,world"中的逗号改为点,但是发现还是不行,程序直接崩溃了。看来go语言中的指针得到了大大的限制,设计者并不想让程序员过度使用指针来写出一些不安全的代码。

3、使用string给另一个string赋值

Golang中的字符串的赋值并不是拷贝底层的字符串数组,而是数组指针和长度字段的拷贝。例如:当我们定义了一个字符串 a := “hello,world” 然后定义了 b := a 底层所做的操作只是创建了两个StringHeader的结构体,它们的Data字段都指向同一段数据,如下图:

在这里插入图片描述

我们可以利用代码来证实这一点:

package main

import (

"fmt"

"reflect"

"unsafe"

)

func main() {

a := "hello,world"

b := a

fmt.Println(a)

fmt.Println(b)

aptr := (*reflect.StringHeader) (unsafe.Pointer(a))

bptr := (*reflect.StringHeader) (unsafe.Pointer(b))

fmt.Println("a ptr:", unsafe.Pointer(aptr.Data))

fmt.Println("b ptr:", unsafe.Pointer(bptr.Data))

}

// 运行结果

hello, world

hello, world

a ptr: 0x6bdb76

b ptr: 0x6bdb76

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

在上面的代码中,将a和b转换为StringHeader类型的指针,然后分别打印出,a和b的Data指针的值,发现是相同的

那么如果对a做切片赋值给b呢?

func main() {

a := "hello,world"

b := a[6:]

fmt.Println(a)

fmt.Println(b)

aptr := (*reflect.StringHeader) (unsafe.Pointer(a))

bptr := (*reflect.StringHeader) (unsafe.Pointer(b))

fmt.Println("a ptr:", unsafe.Pointer(aptr.Data))

fmt.Println("b ptr:", unsafe.Pointer(bptr.Data))

}

// 运行结果

hello,world

world

a ptr: 0xd4d849

b ptr: 0xd4d84f

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

0xd4d849 - 0xd4d84f = 0x000006

显然,也没有分配新的数组并拷贝数据,而是将原字符数组的指针的偏移赋给了b的StringHeader的Data

4、string重新赋值

如果对一个已经赋值的字符串重新赋值,也不会修改原内存空间,而是申请了新的内存空间,对其赋值,并指向新的内存空间。如下图:

在这里插入图片描述

也可以使用代码来证实一下:

package main

import (

"fmt"

"reflect"

"unsafe"

)

func main() {

a := "hello,world"

aptr := (*reflect.StringHeader) (unsafe.Pointer(a))

fmt.Println("a ptr:", unsafe.Pointer(aptr.Data))

fmt.Println("a len", aptr.Len)

a = "hello,golang"

newAPtr := (*reflect.StringHeader) (unsafe.Pointer(a))

fmt.Println("b ptr:", unsafe.Pointer(newAPtr.Data))

fmt.Println("b len:", newAPtr.Len)

}

// 运行结果

a ptr: 0x3ed7f4

a len 11

b ptr: 0x3edb2c

b len: 12

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

文章知识点与官方知识档案匹配

Go技能树九阴真经字符串

2043 人正在系统学习中

点击阅读全文

打开CSDN APP,看更多技术内容

Golang底层原理剖析之string类型与字符编码_cheems~的博客_g...

string类型 string结构 go语言中默认使用的是UTF-8编码 string由两部分组成,一部分是指向字符串起始地址的指针,另一部分是字节个数len注意不是字符个数,是字节个数!这个数据类型占用16B空间,指向字符串起始地址的指针和存在字节个数的整...

继续访问

...底层系列二(基础)_GoGo在努力的博客_golang string底层

1、 string的定义 Golang中的string的定义在reflect包下的value.go中,定义如下: StringHeader 是字符串的运行时表示,其中包含了两个字段,分别是指向数据数组的指针和数组的长度。 // StringHeader is the runtime representation of a...

继续访问

最新发布 Golang: []string 和 ...string

… 参数语法形成了可变参数的参数。它将接受零个或多个string参数,并将它们作为切片引用

继续访问

常用的Golang字符串(string)操作

文章目录Golang中常用的字符串操作一、标准库相关的Package二、常用字符串操作判断是否为空字符串 正文 Golang中常用的字符串操作 一、标准库相关的Package 二、常用字符串操作 判断是否为空字符串 思路:直接判断是否等于""空字符串,由于golang中字符串不能为nil,且为值类型,所以直接与空字符串比较即可 举例: str := "" if str == ""{...

继续访问

golang中的string_xiaodongdonga的博客_golang ...string

golang中的string 在golang中,string其实是个结构体,如图 ,在这个结构体中有两个值,一个是指向字符数组的指针str,另一个是这个字符串的长度len。 另外在golang中用的是UFT8边长编码,里面字母是用一个字节,西方的文字使用两个字节,...

继续访问

Golang中对字符串string的相关操作_Codex_97的博客_go 字符...

golang中拥有string的内置类型,可以使用与普通slice类型相似的性质。 1.简介 strings包提供了很多操作字符串的简单函数,通常一般的字符串操作都可以在这个包中找到。 strconv 包提供了基本数据类型和字符串之间的转换。在Go 中,没有隐式...

继续访问

go-string-set:一种在GoLang中创建字符串集的方法

概述 通过字符串设置方法。 用法 go get github.com/wojnosystems/go-string-set package main import ( "github.com/wojnosystems/go-string-set/string_set" "strings" ) func main () { myPeople := string_set . New () myPeople . Add ( "Bob" ) myPeople . Add ( "Jane" ) myPeople . Add ( "Gary" ) myPeople . Add ( "Bob" ) if myPeople . Exists ( "Bob" ) { // Bob exists in myPeople, so this code block execute

Golang:strings包和字符串操作

strings包 string类型值是不可变的,如果想要获得一个不一样的字符串,就只能基于原字符串进行剪裁,拼接等操作,然后将得到的新字符串存放到一块联系内存中。 string值包含了指向底层字节数组头部的指针值,以及该字节数组的长度。在string值上做切片,就相当于在其底层字节数组做切片。 而字符串拼接时(使用+),会把所有被拼接的字符串依次拷贝到一个崭新且足够大的连续内存空间中,并把持有新...

继续访问

golang中的strings.SplitN_盼盼编程的博客_strings.splitn

golang中的字符串操作strings.SplitN package main import ( "fmt" "strings" ) //golang字符串操作 func main(){ s := "hello world hello world" //str := "wo" //以str为分隔符,将s切分成多个子串,结果中**不包含*...

继续访问

Golang_String常用操作_itzhuzhu.的博客_golang strings

func main() { var x string = "ITzhuzhu" var y string = "itzhuzhu" fmt.Println(strings.EqualFold(x, y)) //true fmt.Println(x == y) //false } 1 2 3 4 5 6 返回子串在字符串第一次出现的索引,如果没有则...

继续访问

Golang类型的String()方法

作用 用于定制fmt.Println(x)、fmt.Printf("%v", x)、fmt.Print(x)时输出的内容。 示例 package main import "fmt" type Couple struct { Husband string Wife string } func (self Couple) String() string { return "(husband:" + self.Husband + ", wife:" + self.Wife +

继续访问

golang的string

golang中的string是不可变的字节序列,零值是空字符串,默认是UTF-8编码。 golang中使用字符串最需要注意的是,golang中的字符串是字节序列,string == []byte,由于一个字符占用的字节数不确定,所以无法通过下标[i]的方式稳定地获取对应位置的字符。 也就意味着字符串的第i个字节,不一定就是第i个字符,原因请参考 unicode与UTF-8 文字符号在golang中被称为rune,发音为/ru:n/,意思是符号。由于utf-8最多使用4个字节来编码,所以rune类型是int3

继续访问

golang中的strings.Trim_盼盼编程的博客_strings.trim

golang中的字符串操作strings.Trim package main import ( "fmt" "strings" ) //golang字符串操作 func main(){ s := "Hello world hello world" str := "world" //var s = []string{"11","22","33"} //删除s首尾...

继续访问

Golang 字符串

Go语言的代码是由Unicode字符组成的,它们都必须由Unicode编码规范中的UTF-8编码格式进行编码并存储,Unicode编码规范中的编码格式定义的是字符与字节序列之间的转换方式。其中的UTF-8是一种可变宽的编码方案,它会用一个或多个字节的二进制数来表示某个字符,最多使用四个字节。Go语言中的一个string类型值会由若干个Unicode字符组成,每个Unicode字符都可以由一个rune类型的值来承载。stringforrange。...

继续访问

golang字符串[]string(slice)去重

1、现实代码 // []string 去重 func RemoveDuplicate(list []string) []string { // 这个排序很关键 sort.Strings(list) i := 0 var newlist = []string{""} for j := 0; j len(list); j++ { if strings.Compare(newlist[i], list[j]) == -1 { newlist = append(newlist, lis

继续访问

深入理解golang string

golang string string的定义 // string is the set of all strings of 8-bit bytes, conventionally but not // necessarily representing UTF-8-encoded text. A string may be empty, but // not nil. Values of string type are immutable. type string string string里存储的是字符

继续访问

golang中string包

对于基本类型来说,字符串所需要执行的操作会比较复杂,所以一般语言都会额外封装一些方法用于处理字符串,go语言标准库中也存在这样一个名为strings的库 包含判断,判断一个字符中是否有相应的某个子字符串是经常遇到的一种字符串操作,再go语言中可以使用strings包中的两个方法判断 ...

继续访问

golang中的strings.TrimLeft

golang中的字符串操作strings.TrimLeft package main import ( "fmt" "strings" ) //golang字符串操作 func main(){ s := "Hello world hello world" str := "Hello" //var s = []string{"11","22","33"} //删除s头部连续的包含在str中的字符串 .

继续访问

golang入门time与string转换, time加减时间, 两个时间差

package mainimport ( "fmt" "time")var timeLayoutStr = "2006-01-02 15:04:05" //go中的时间格式化必须是这个时间//var timeLayoutStr = 2006/01/02 03:04:05 //合法, 格式可以改变//var timeLayoutStr = 2019/01/02 15:04:05 /...

继续访问

Golang——string

1.string结构体底层 由两部分组成:指向底层[ ]byte数组的指针;长度(类似切片结构体) 2.相同的string常量不会重复存储 //由s1、s2的指针指向一块相同的只读内存,这段内存以utf-8编码存放hello数组 s1 := "hello" s2 := "hello" 3.string常量会在编译期分配到只读段,对应数据地址不可写入,故string不支持修改。 要修改必须转[]byte,string和[]byte转换,会将这段只读内存的数据复制到堆/栈上。 //wrong s := "he

继续访问

热门推荐 golang -----------字符串(rune,string,type)

一、内存布局 字符串在Go语言内存模型中用一个2字长的数据结构表示。它包含一个指向字符串存储数据的指针和一个长度数据。因为string类型是不可变的,对于多字符串共享同一个存储数据是安全的。切分操作str[i:j]会得到一个新的2字长结构,一个可能不同的但仍指向同一个字节序列(即上文说的存储数据)的指针和长度数据。这意味着字符串切分可以在不涉及内存分配或复制操作。这使得字符串切分的效率等同于...

继续访问

Golang string 常用方法

strings.Compare(p1, p2) 判断p1是否大于p2,大于:1,小于:-1,等于:0。2.strings.LastIndex(p1,p2) p2在p1中最后一次出现的位置,未出现返回-1。1.strings.Replace(p1,p2,p3,n) 将p1中的p2替换成p3,最多替换n个。1.strings.Index(p1,p2) p2在p1中第一次出现的位置,未出现返回-1。2.strings.ReplaceAll(p1,p2,p3) 将p1中的所有p2替换成p3。

继续访问

golang中的字符串

在go中rune是一个unicode编码点。 我们都知道UTF-8将字符编码为1-4个字节,比如我们常用的汉字,UTF-8编码为3个字节。所以rune也是int32的别名。

继续访问

golang之字符串

Go语言的字符有两种类型: 一种是byte型,或者叫uint8类型,代表了ASCII码的一个字符。 一种是rune类型,或者叫int32类型,代表一个UTF-8字符,当需要处理中文、日文等unicode字符时,则需要用到rune类型。 ...

继续访问

Golang的 string 类型

一点睛 字符串就是一串固定长度的字符连接起来的字符序列。Go 的字符串是由单个字节连接起来的。Go语言的字符串的字节使用 UTF-8 编码标识 Unicode 文本。 二 string 使用注意事项和细节 1 Go 语言的字符串的字节使用 UTF-8 编码标识 Unicode 文本,这样 Golang 统一使用 UTF-8 编码,中文乱码问题不会再困扰程序员。 2 字符串一旦赋值了,字符串就不能修改了,在 Go 中字符串是不可变的。 3 字符串的两种表示形式。 a 双引号——会识别转义字符 .

继续访问

Golang Strings 官方包常用方法,学会这些够用了

1.判断是否以某字符串打头/结尾 2.字符串分割 3.返回子串索引 4.字符串连接 5.字符串转化为大小写 6.统计某个字符在字符串出现的次数 7.判断字符串的包含关系 8.字符串替换

为什么要使用 Go 语言?Go 语言的优势在哪里

1. 保留但大幅度简化指针

Go语言保留着C中值和指针的区别,但是对于指针繁琐用法进行了大量的简化,引入引用的概念。所以在Go语言中,你几乎不用担心会因为直接操作内寸而引起各式各样的错误。

2. 多参数返回

还记得在C里面为了回馈多个参数,不得不开辟几段指针传到目标函数中让其操作么?在Go里面这是完全不必要的。而且多参数的支持让Go无需使用繁琐的exceptions体系,一个函数可以返回期待的返回值加上error,调用函数后立刻处理错误信息,清晰明了。

3. Array,slice,map等内置基本数据结构

如果你习惯了Python中简洁的list和dict操作,在Go语言中,你不会感到孤单。一切都是那么熟悉,而且更加高效。如果你是C++程序员,你会发现你又找到了STL的vector 和 map这对朋友。

4. Interface

Go语言最让人赞叹不易的特性,就是interface的设计。任何数据结构,只要实现了interface所定义的函数,自动就implement了这个interface,没有像Java那样冗长的class申明,提供了灵活太多的设计度和OO抽象度,让你的代码也非常干净。千万不要以为你习惯了Java那种一条一条加implements的方式,感觉还行,等接口的设计越来越复杂的时候,无数Bug正在后面等着你。

同时,正因为如此,Go语言的interface可以用来表示任何generic的东西,比如一个空的interface,可以是string可以是int,可以是任何数据类型,因为这些数据类型都不需要实现任何函数,自然就满足空interface的定义了。加上Go语言的type assertion,可以提供一般动态语言才有的duck typing特性, 而仍然能在compile中捕捉明显的错误。

5. OO

Go语言本质上不是面向对象语言,它还是过程化的。但是,在Go语言中, 你可以很轻易的做大部分你在别的OO语言中能做的事,用更简单清晰的逻辑。是的,在这里,不需要class,仍然可以继承,仍然可以多态,但是速度却快得多。因为本质上,OO在Go语言中,就是普通的struct操作。

6. Goroutine

这个几乎算是Go语言的招牌特性之一了,我也不想多提。如果你完全不了解Goroutine,那么你只需要知道,这玩意是超级轻量级的类似线程的东西,但通过它,你不需要复杂的线程操作锁操作,不需要care调度,就能玩转基本的并行程序。在Go语言里,触发一个routine和erlang spawn一样简单。基本上要掌握Go语言,以Goroutine和channel为核心的内存模型是必须要懂的。不过请放心,真的非常简单。

7. 更多现代的特性

和C比较,Go语言完全就是一门现代化语言,原生支持的Unicode, garbage collection, Closures(是的,和functional programming language类似), function是first class object,等等等等。

看到这里,你可能会发现,我用了很多轻易,简单,快速之类的形容词来形容Go语言的特点。我想说的是,一点都不夸张,连Go语言的入门学习到提高,都比别的语言门槛低太多太多。在大部分人都有C的背景的时代,对于Go语言,从入门到能够上手做项目,最多不过半个月。Go语言给人的感觉就是太直接了,什么都直接,读源代码直接,写自己的代码也直接。

Go语言中恰到好处的内存对齐

在开始之前,希望你计算一下 Part1 共占用的大小是多少呢?

输出结果:

这么一算, Part1 这一个结构体的占用内存大小为 1+4+1+8+1 = 15 个字节。相信有的小伙伴是这么算的,看上去也没什么毛病

真实情况是怎么样的呢?我们实际调用看看,如下:

输出结果:

最终输出为占用 32 个字节。这与前面所预期的结果完全不一样。这充分地说明了先前的计算方式是错误的。为什么呢?

在这里要提到 “内存对齐” 这一概念,才能够用正确的姿势去计算,接下来我们详细的讲讲它是什么

有的小伙伴可能会认为内存读取,就是一个简单的字节数组摆放

上图表示一个坑一个萝卜的内存读取方式。但实际上 CPU 并不会以一个一个字节去读取和写入内存。相反 CPU 读取内存是 一块一块读取 的,块的大小可以为 2、4、6、8、16 字节等大小。块大小我们称其为 内存访问粒度 。如下图:

在样例中,假设访问粒度为 4。 CPU 是以每 4 个字节大小的访问粒度去读取和写入内存的。这才是正确的姿势

另外作为一个工程师,你也很有必要学习这块知识点哦 :)

在上图中,假设从 Index 1 开始读取,将会出现很崩溃的问题。因为它的内存访问边界是不对齐的。因此 CPU 会做一些额外的处理工作。如下:

从上述流程可得出,不做 “内存对齐” 是一件有点 "麻烦" 的事。因为它会增加许多耗费时间的动作

而假设做了内存对齐,从 Index 0 开始读取 4 个字节,只需要读取一次,也不需要额外的运算。这显然高效很多,是标准的 空间换时间 做法

在不同平台上的编译器都有自己默认的 “对齐系数”,可通过预编译命令 #pragma pack(n) 进行变更,n 就是代指 “对齐系数”。一般来讲,我们常用的平台的系数如下:

另外要注意,不同硬件平台占用的大小和对齐值都可能是不一样的。因此本文的值不是唯一的,调试的时候需按本机的实际情况考虑

输出结果:

在 Go 中可以调用 unsafe.Alignof 来返回相应类型的对齐系数。通过观察输出结果,可得知基本都是 2^n ,最大也不会超过 8。这是因为我手提(64 位)编译器默认对齐系数是 8,因此最大值不会超过这个数

在上小节中,提到了结构体中的成员变量要做字节对齐。那么想当然身为最终结果的结构体,也是需要做字节对齐的

接下来我们一起分析一下,“它” 到底经历了些什么,影响了 “预期” 结果

在每个成员变量进行对齐后,根据规则 2,整个结构体本身也要进行字节对齐,因为可发现它可能并不是 2^n ,不是偶数倍。显然不符合对齐的规则

根据规则 2,可得出对齐值为 8。现在的偏移量为 25,不是 8 的整倍数。因此确定偏移量为 32。对结构体进行对齐

Part1 内存布局:axxx|bbbb|cxxx|xxxx|dddd|dddd|exxx|xxxx

通过本节的分析,可得知先前的 “推算” 为什么错误?

是因为实际内存管理并非 “一个萝卜一个坑” 的思想。而是一块一块。通过空间换时间(效率)的思想来完成这块读取、写入。另外也需要兼顾不同平台的内存操作情况

在上一小节,可得知根据成员变量的类型不同,其结构体的内存会产生对齐等动作。那假设字段顺序不同,会不会有什么变化呢?我们一起来试试吧 :-)

输出结果:

通过结果可以惊喜的发现,只是 “简单” 对成员变量的字段顺序进行改变,就改变了结构体占用大小

接下来我们一起剖析一下 Part2 ,看看它的内部到底和上一位之间有什么区别,才导致了这样的结果?

符合规则 2,不需要额外对齐

Part2 内存布局:ecax|bbbb|dddd|dddd

通过对比 Part1 和 Part2 的内存布局,你会发现两者有很大的不同。如下:

仔细一看, Part1 存在许多 Padding。显然它占据了不少空间,那么 Padding 是怎么出现的呢?

通过本文的介绍,可得知是由于不同类型导致需要进行字节对齐,以此保证内存的访问边界

那么也不难理解,为什么 调整结构体内成员变量的字段顺序 就能达到缩小结构体占用大小的疑问了,是因为巧妙地减少了 Padding 的存在。让它们更 “紧凑” 了。这一点对于加深 Go 的内存布局印象和大对象的优化非常有帮

分享标题:了解go语言中的内存模型 go语言内存占用
本文地址:https://www.cdcxhl.com/article12/ddoeidc.html

成都网站建设公司_创新互联,为您提供面包屑导航关键词优化小程序开发企业建站网站建设App开发

广告

声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联

成都网站建设