欧美一区二区三区老妇人-欧美做爰猛烈大尺度电-99久久夜色精品国产亚洲a-亚洲福利视频一区二区

golang中經(jīng)常會(huì)犯的一些錯(cuò)誤

0.1、索引

https://waterflow.link/articles/

創(chuàng)新互聯(lián)專注于三亞企業(yè)網(wǎng)站建設(shè),響應(yīng)式網(wǎng)站開發(fā),商城系統(tǒng)網(wǎng)站開發(fā)。三亞網(wǎng)站建設(shè)公司,為三亞等地區(qū)提供建站服務(wù)。全流程按需網(wǎng)站策劃,專業(yè)設(shè)計(jì),全程項(xiàng)目跟蹤,創(chuàng)新互聯(lián)專業(yè)和態(tài)度為您提供的服務(wù)

1、未知的枚舉值

我們現(xiàn)在定義一個(gè)類型是unit32的Status,他可以作為枚舉類型,我們定義了3種狀態(tài)

type Status uint32

const (
	StatusOpen Status = iota
	StatusClosed
	StatusUnknown
)

其中我們使用了iota,相關(guān)的用法自行g(shù)oogle。最終對(duì)應(yīng)的狀態(tài)就是:

0-開啟狀態(tài),1-關(guān)閉狀態(tài),2-未知狀態(tài)

現(xiàn)在我們假設(shè)有一個(gè)請(qǐng)求參數(shù)過來,數(shù)據(jù)結(jié)構(gòu)如下:

{
  "Id": 1234,
  "Timestamp": ,
  "Status": 1
}

可以看到是一個(gè)json類型的字符串,其中就包含了Status狀態(tài),我們的請(qǐng)求是希望把狀態(tài)修改為關(guān)閉狀態(tài)。

然后我們?cè)诜?wù)端創(chuàng)建一個(gè)結(jié)構(gòu)體,方便把這些字段解析出來:

type Request struct {
	ID        int    `json:"Id"`
	Timestamp int    `json:"Timestamp"`
	Status    Status `json:"Status"`
}

好了,我們?cè)趍ain中執(zhí)行下代碼,看下解析是否正確:

package main

import (
	"encoding/json"
	"fmt"
)

type Status uint32

const (
	StatusOpen Status = iota
	StatusClosed
	StatusUnknown
)

type Request struct {
	ID        int    `json:"Id"`
	Timestamp int    `json:"Timestamp"`
	Status    Status `json:"Status"`
}

func main() {
	js := `{
		"Id": 1234,
		"Timestamp": ,
		"Status": 1
	  }`

	request := &Request{}
	err := json.Unmarshal([]byte(js), request)
	if err != nil {
		fmt.Println(err)
		return
	}
}

執(zhí)行后的結(jié)果如下:

go run main.go
&{1234  1}

可以看到解析是沒問題的。

然而,讓我們?cè)偬岢鲆粋€(gè)未設(shè)置狀態(tài)值的請(qǐng)求(無論出于何種原因):

{
  "Id": 1234,
  "Timestamp": 
}

在這種情況下,請(qǐng)求結(jié)構(gòu)的狀態(tài)字段將被初始化為其零值(對(duì)于 uint32 類型:0)。因此,StatusOpen 而不是 StatusUnknown。

最佳實(shí)踐是將枚舉的未知值設(shè)置為 0:

type Status uint32

const (
	StatusUnknown Status = iota
	StatusOpen
	StatusClosed
)

在這里,如果狀態(tài)不是 JSON 請(qǐng)求的一部分,它將被初始化為 StatusUnknown,正如我們所期望的那樣。

2、指針無處不在?

按值傳遞變量將創(chuàng)建此變量的副本。而通過指針傳遞它只會(huì)復(fù)制內(nèi)存地址。

因此,傳遞指針總是會(huì)更快,對(duì)么?

如果你相信這一點(diǎn),請(qǐng)看看這個(gè)例子。這是一個(gè) 0.3 KB 數(shù)據(jù)結(jié)構(gòu)的基準(zhǔn)測(cè)試,我們通過指針和值傳遞和接收。 0.3 KB 并不大,但這與我們每天看到的數(shù)據(jù)結(jié)構(gòu)類型(對(duì)于我們大多數(shù)人來說)應(yīng)該相差不遠(yuǎn)。

當(dāng)我在本地環(huán)境中執(zhí)行這些基準(zhǔn)測(cè)試時(shí),按值傳遞比按指針傳遞快 4 倍以上。這可能有點(diǎn)違反直覺,對(duì)吧?

這其實(shí)與 Go 中如何管理內(nèi)存有關(guān)。我們都知道變量可以分配在堆上或棧上,也知道:

  • 棧包含給定 goroutine 的正在進(jìn)行的變量。一旦函數(shù)返回,變量就會(huì)從堆棧中彈出。
  • 堆包含共享變量(全局變量等)。

讓我們看下下面這個(gè)簡單的例子:

type foo struct{}

func getFooValue() foo {
	var result foo
	// Do something
	return result
}

這里,一個(gè)結(jié)果變量由當(dāng)前的 goroutine 創(chuàng)建。這個(gè)變量被壓入當(dāng)前堆棧。一旦函數(shù)返回,客戶端將收到此變量的副本。變量本身從堆棧中彈出。它仍然存在于內(nèi)存中,直到它被另一個(gè)變量擦除,但它不能再被訪問。

我們現(xiàn)在修改下上面的例子,使用指針:

type foo struct{}

func getFooPointer() *foo {
	var result foo
	// Do something
	return &result
}

結(jié)果變量仍然由當(dāng)前的 goroutine 創(chuàng)建,但客戶端將收到一個(gè)指針(變量地址的副本)。如果結(jié)果變量從堆棧中彈出,則此函數(shù)的客戶端無法再訪問它。

在這種情況下,Go 編譯器會(huì)將結(jié)果變量轉(zhuǎn)移到可以共享變量的地方:堆。

但是,傳遞指針是另一種情況。例如:

type foo struct{}

func main()  {
	p := &foo{}
	f(p)
}

因?yàn)槲覀冊(cè)谕粋€(gè) goroutine 中調(diào)用 f,所以 p 變量不需要被轉(zhuǎn)移。它只是被壓入堆棧,子函數(shù)可以訪問它。

比如在 io.Reader 的 Read 方法中接收切片而不是返回切片的直接結(jié)果,也不會(huì)轉(zhuǎn)移到堆上。

但是返回一個(gè)切片(它是一個(gè)指針)會(huì)將其轉(zhuǎn)移到堆中。

為什么堆棧那么快?主要原因有兩個(gè):

  • 堆棧不需要垃圾收集器。正如我們所說,一個(gè)變量在創(chuàng)建后被簡單地壓入,然后在函數(shù)返回時(shí)從堆棧中彈出。無需進(jìn)行復(fù)雜的過程來回收未使用的變量等。
  • 堆棧屬于一個(gè) goroutine,因此與將變量存儲(chǔ)在堆上相比,存儲(chǔ)變量不需要同步。這也導(dǎo)致性能增益。

結(jié)論就是:

當(dāng)我們創(chuàng)建一個(gè)函數(shù)時(shí),我們的默認(rèn)行為應(yīng)該是使用值而不是指針。僅當(dāng)我們想要共享變量時(shí)才應(yīng)使用指針。

最后:

如果我們遇到性能問題,一種可能的優(yōu)化可能是檢查指針在某些特定情況下是否有幫助。使用以下命令可以知道編譯器何時(shí)將變量轉(zhuǎn)移到堆中:go build -gcflags "-m -m"。(內(nèi)存逃逸)

3、中斷 for/switch 或 for/select

我們看下下面的代碼會(huì)發(fā)生什么:

package main

func f() bool {
	return true
}

func main() {
	for {
		switch f() {
		case true:
			break
		case false:
			// Do something
		}
	}
}

我們將調(diào)用 break 語句。但是,這會(huì)破壞 switch 語句,而不是 for 循環(huán)。

相同的情況還會(huì)出現(xiàn)在fo/select中,像下面這樣:

package main

import (
	"context"
	"time"
)

func main() {
	ch := make(chan struct{})
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()
	for {
		select {
		case <-ch:
		// Do something
		case <-ctx.Done():
			break
		}
	}
}

雖然調(diào)用了break,但是還是會(huì)陷入死循環(huán)。break 與 select 語句有關(guān),與 for 循環(huán)無關(guān)。

打破 for/switch 或 for/select 的,一種方案是直接return結(jié)束整個(gè)函數(shù),下面如果還有代碼不會(huì)被執(zhí)行。

package main

import (
	"context"
	"fmt"
	"time"
)

func main() {
	ch := make(chan struct{})
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()
	for {
		select {
		case <-ch:
		// Do something
		case <-ctx.Done():
			return
		}
	}

  // 這里不會(huì)執(zhí)行
	fmt.Println("done")
}

還有一種方案是使用中斷標(biāo)記

package main

import (
	"context"
	"fmt"
	"time"
)

func main() {
	ch := make(chan struct{})
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()
loop:
	for {
		select {
		case <-ch:
		// Do something
		case <-ctx.Done():
			break loop
		}
	}

  // 會(huì)繼續(xù)往下執(zhí)行
	fmt.Println("done")
}

4、錯(cuò)誤管理

一個(gè)錯(cuò)誤應(yīng)該只處理一次。記錄錯(cuò)誤就是處理錯(cuò)誤。因此,應(yīng)該記錄或傳播錯(cuò)誤。

我們可能希望為錯(cuò)誤添加一些上下文并具有某種形式的層次結(jié)構(gòu)。

讓我們看一個(gè)接口請(qǐng)求數(shù)據(jù)庫的例子,我們分為接口層,service層和類庫層。我們希望返回的層次結(jié)構(gòu)像下面這樣:

unable to serve HTTP POST request for id 1
 |_ unable to insert customer
     |_ unable to commit transaction

如果我們使用 pkg/errors,我們可以這樣做:

package main

import (
	"fmt"

	"github.com/pkg/errors"
)

func postHandler(id int) string {
	err := insert(id)
	if err != nil {
		fmt.Printf("unable to serve HTTP POST request for id %d\n", id)
		return `{ok: false}`
	}
	return `{ok: true}`
}

func insert(id int) error {
	err := dbQuery(id)
	if err != nil {
		return errors.Wrapf(err, "unable to insert customer")
	}
	return nil
}

func dbQuery(id int) error {
	// Do something then fail
	return errors.New("unable to commit transaction")
}

func main() {
	res := postHandler(1)
	fmt.Println(res)
}

初始錯(cuò)誤(如果不是由外部庫返回)可以使用 errors.New 創(chuàng)建。service層 insert 通過向其添加更多上下文來包裝此錯(cuò)誤。然后,接口層通過記錄錯(cuò)誤來處理錯(cuò)誤。每個(gè)級(jí)別都返回或處理錯(cuò)誤。

例如,我們可能還想檢查錯(cuò)誤原因本身以實(shí)現(xiàn)重試。假設(shè)我們有一個(gè)來自處理數(shù)據(jù)庫訪問的外部庫的 db 包。這個(gè)庫可能會(huì)返回一個(gè)名為 db.DBError 的暫時(shí)(臨時(shí))錯(cuò)誤。要確定是否需要重試,我們必須檢查錯(cuò)誤原因:

package main

import (
	"fmt"

	"github.com/pkg/errors"
)

type DbError struct {
	msg string
}

func (e *DbError) Error() string {
	return e.msg
}

func postHandler(id int) string {
	err := insert(id)
	if err != nil {
		errCause := errors.Cause(err)
		if _, ok := errCause.(*DbError); ok {
			fmt.Println("retry")
		} else {
			fmt.Printf("unable to serve HTTP POST request for id %d\n", id)
			return `{ok: false}`
		}

	}
	return `{ok: true}`
}

func insert(id int) error {
	err := dbQuery(id)
	if err != nil {
		return errors.Wrapf(err, "unable to insert customer")
	}
	return nil
}

func dbQuery(id int) error {
	// Do something then fail
	return &DbError{"unable to commit transaction"}
}

func main() {
	res := postHandler(1)
	fmt.Println(res)
}

這是使用errors.Cause完成的,它也來自pkg/errors。(可以通過errors.Cause檢查。 errors.Cause 將遞歸檢索沒有實(shí)現(xiàn)causer 的最頂層錯(cuò)誤,這被認(rèn)為是原始原因。)

有時(shí)候也會(huì)有人這么用。例如,檢查錯(cuò)誤是這樣完成的:

package main

import (
	"fmt"

	"github.com/pkg/errors"
)

type DbError struct {
	msg string
}

func (e *DbError) Error() string {
	return e.msg
}

func postHandler(id int) string {
	err := insert(id)
	if err != nil {
		switch err.(type) {
		default:
			fmt.Printf("unable to serve HTTP POST request for id %d\n", id)
			return `{ok: false}`
		case *DbError:
			fmt.Println("retry")

		}
	}
	return `{ok: true}`
}

func insert(id int) error {
	err := dbQuery(id)
	if err != nil {
		return errors.Wrapf(err, "unable to insert customer")
	}
	return nil
}

func dbQuery(id int) error {
	// Do something then fail
	return &DbError{"unable to commit transaction"}
}

func main() {
	res := postHandler(1)
	fmt.Println(res)
}

如果 DBError 被包裝,它永遠(yuǎn)不會(huì)觸發(fā)重試。

5、切片初始化

有時(shí),我們知道切片的最終長度是多少。例如,假設(shè)我們要將 Foo 的切片轉(zhuǎn)換為 Bar 的切片,這意味著這兩個(gè)切片將具有相同的長度。

我們有時(shí)候經(jīng)常會(huì)這樣初始化切片:

var bars []Bar
bars := make([]Bar, 0)

我們都知道切片的底層是數(shù)組。如果沒有更多可用空間,它會(huì)實(shí)施增長戰(zhàn)略。在這種情況下,會(huì)自動(dòng)創(chuàng)建一個(gè)新數(shù)組(容量更大)并復(fù)制所有元素。

現(xiàn)在,假設(shè)我們需要多次重復(fù)這個(gè)增長操作,因?yàn)槲覀兊?[]Foo 包含數(shù)千個(gè)元素?插入的攤銷時(shí)間復(fù)雜度(平均值)將保持為 O(1),但在實(shí)踐中,它會(huì)對(duì)性能產(chǎn)生影響。

因此,如果我們知道最終長度,我們可以:

  • 使用預(yù)定義的長度對(duì)其進(jìn)行初始化:

    func convert(foos []Foo) []Bar {
    	bars := make([]Bar, len(foos))
    	for i, foo := range foos {
    		bars[i] = fooToBar(foo)
    	}
    	return bars
    }
    
  • 或者使用 0 長度和預(yù)定義容量對(duì)其進(jìn)行初始化:

    func convert(foos []Foo) []Bar {
    	bars := make([]Bar, 0, len(foos))
    	for _, foo := range foos {
    		bars = append(bars, fooToBar(foo))
    	}
    	return bars
    }
    

選哪個(gè)更好呢?第一個(gè)稍微快一點(diǎn)。然而,你可能更喜歡第二個(gè),因?yàn)闊o論我們是否知道初始大小,在切片末尾添加一個(gè)元素都是使用 append 完成的。

6、上下文管理

context.Context對(duì)我們來說非常好用,他可以在協(xié)程之間傳遞數(shù)據(jù)、可以控制協(xié)程的生命周期等等。但是這也造成了它的濫用。

go官方文檔是這么定義的:

一個(gè) Context 攜帶一個(gè)截止日期、一個(gè)取消信號(hào)和其他跨 API 邊界的值。

這個(gè)描述很寬泛,足以讓一些人對(duì)為什么以及如何使用它感到困惑。

讓我們?cè)囍敿?xì)說明一下。上下文可以攜帶:

  • 一個(gè)截止時(shí)間。它意味著一個(gè)持續(xù)時(shí)間(例如 250 毫秒)或日期時(shí)間(例如 2022-01-08 01:00:00),我們認(rèn)為如果達(dá)到,我們必須取消正在進(jìn)行的活動(dòng)(I/O 請(qǐng)求,等待通道輸入等)。
  • 取消信號(hào)(基本上是 <-chan struct{})。 在這里,行為是相似的。 一旦我們收到信號(hào),我們必須停止正在進(jìn)行的活動(dòng)。 例如,假設(shè)我們收到兩個(gè)請(qǐng)求。 一個(gè)插入一些數(shù)據(jù),另一個(gè)取消第一個(gè)請(qǐng)求(因?yàn)樗辉傩枰?這可以通過在第一次調(diào)用中使用可取消上下文來實(shí)現(xiàn),一旦我們收到第二個(gè)請(qǐng)求,該上下文將被取消。
  • 鍵/值列表(均基于 interface{} 類型)。

另外需要說明的是。

首先,上下文是可組合的。因此,我們可以有一個(gè)包含截止日期和鍵/值列表的上下文。

此外,多個(gè) goroutine 可以共享相同的上下文,因此取消信號(hào)可能會(huì)停止多個(gè)活動(dòng)。

我們可以看下一個(gè)具體的錯(cuò)誤例子

一個(gè) Go 應(yīng)用程序是基于 urfave/cli 的(如果你不知道,那是一個(gè)在 Go 中創(chuàng)建命令行應(yīng)用程序的好庫)。一旦開始,開發(fā)人員就會(huì)繼承某種應(yīng)用程序上下文。這意味著當(dāng)應(yīng)用程序停止時(shí),庫將使用此上下文發(fā)送取消信號(hào)。

我了解的是,這個(gè)上下文是在調(diào)用 gRPC 端點(diǎn)時(shí)直接傳遞的。這不是我們想要做的。

相反,我們想向 gRPC 庫傳遞:請(qǐng)?jiān)趹?yīng)用程序停止時(shí)或在 100 毫秒后取消請(qǐng)求。

為此,我們可以簡單地創(chuàng)建一個(gè)組合上下文。如果 parent 是應(yīng)用程序上下文的名稱(由 urfave/cli 創(chuàng)建),那么我們可以簡單地這樣做:

package main

import (
	"context"
	"fmt"
	"log"
	"os"
	"time"

	"github.com/urfave/cli/v2"
)

func main() {

	app := &cli.App{
		Name:  "boom",
		Usage: "make an explosive entrance",
		Action: func(parent *cli.Context) error {
      // 父上下文傳進(jìn)來,給個(gè)超時(shí)時(shí)間
			ctx, cancel := context.WithTimeout(parent.Context, 10*time.Second)
			defer cancel()
			grpcClientSend(ctx)

			return nil
		},
	}

	if err := app.Run(os.Args); err != nil {
		log.Fatal(err)
	}
}

func grpcClientSend(ctx context.Context) {
	for {
		select {
		case <-ctx.Done(): // 達(dá)到超時(shí)時(shí)間就結(jié)束
			fmt.Println("cancel!")
			return
		default:
			time.Sleep(2 * time.Second)
			fmt.Println("do something!")
		}
	}
}

7、使用文件名作為函數(shù)輸入?

假設(shè)我們必須實(shí)現(xiàn)一個(gè)函數(shù)來計(jì)算文件中的空行數(shù)。一般我們是這樣實(shí)現(xiàn)的:

package main

import (
	"bufio"
	"fmt"
	"os"

	"github.com/pkg/errors"
)

func main() {

	cou, err := count("a.txt")
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(cou)
}

func count(filename string) (int, error) {
	file, err := os.Open(filename)
	if err != nil {
		return 0, errors.Wrapf(err, "unable to open %s", filename)
	}
	defer file.Close()

	scanner := bufio.NewScanner(file)
	count := 0
	for scanner.Scan() {
		if scanner.Text() == "" {
			count++
		}
	}
	return count, nil
}

文件名作為輸入給出,所以我們打開它然后我們實(shí)現(xiàn)我們的邏輯,對(duì)吧?

現(xiàn)在,假設(shè)我們要在此函數(shù)之上實(shí)現(xiàn)單元測(cè)試,以測(cè)試普通文件、空文件、具有不同編碼類型的文件等。這很容易變得非常難以管理。

此外,如果我們想要對(duì)http body實(shí)現(xiàn)相同的邏輯,我們將不得不為此創(chuàng)建另一個(gè)函數(shù)。

Go 帶有兩個(gè)很棒的抽象:io.Reader 和 io.Writer。我們可以簡單地傳遞一個(gè) io.Reader 來抽象數(shù)據(jù)源,而不是傳遞文件名。

是文件嗎? HTTP body?字節(jié)緩沖區(qū)?這并不重要,因?yàn)槲覀內(nèi)詫⑹褂孟嗤?Read 方法。

在我們的例子中,我們甚至可以緩沖輸入以逐行讀取。因此,我們可以使用 bufio.Reader 及其 ReadLine 方法:

我們把讀取文件的部分放到函數(shù)外面

package main

import (
	"bufio"
	"fmt"
	"io"
	"os"

	"github.com/pkg/errors"
)

func main() {

	filename := "a.txt"
	file, err := os.Open(filename)
	if err != nil {
		fmt.Println(err, "unable to open ", filename)
		return
	}
	defer file.Close()
	count, err := count(bufio.NewReader(file))
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(count)
}

func count(reader *bufio.Reader) (int, error) {
	count := 0
	for {
		line, _, err := reader.ReadLine()
		if err != nil {
			switch err {
			default:
				return 0, errors.Wrapf(err, "unable to read")
			case io.EOF:
				return count, nil
			}
		}
		if len(line) == 0 {
			count++
		}
	}
}

使用第二種實(shí)現(xiàn),無論實(shí)際數(shù)據(jù)源如何,都可以調(diào)用該函數(shù)。同時(shí),這將有助于我們的單元測(cè)試,因?yàn)槲覀兛梢院唵蔚貜淖址畡?chuàng)建一個(gè) bufio.Reader:

package main

import (
	"bufio"
	"fmt"
	"io"
	"strings"

	"github.com/pkg/errors"
)

func main() {

	count, err := count(bufio.NewReader(strings.NewReader("input\n\n")))

	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(count)
}

func count(reader *bufio.Reader) (int, error) {
	count := 0
	for {
		line, _, err := reader.ReadLine()
		if err != nil {
			switch err {
			default:
				return 0, errors.Wrapf(err, "unable to read")
			case io.EOF:
				return count, nil
			}
		}
		if len(line) == 0 {
			count++
		}
	}
}

8、Goroutines 和循環(huán)變量

我看到一個(gè)常見錯(cuò)誤是使用帶有循環(huán)變量的 goroutines。

以下示例的輸出是什么?

package main

import (
	"fmt"
	"time"
)

func main() {

	ints := []int{1, 2, 3}
	for _, i := range ints {
		go func() {
			fmt.Printf("%v\n", i)
		}()
	}

	time.Sleep(time.Second)
}

在這個(gè)例子中,每個(gè) goroutine 共享相同的變量實(shí)例,所以它會(huì)產(chǎn)生 3 3 3。而不是我們認(rèn)為的1 2 3

有兩種解決方案可以解決這個(gè)問題。第一個(gè)是將 i 變量的值傳遞給閉包(內(nèi)部函數(shù)):

package main

import (
	"fmt"
	"time"
)

func main() {

	ints := []int{1, 2, 3}
	for _, i := range ints {
		go func(i int) {
			fmt.Printf("%v\n", i)
		}(i)
	}

	time.Sleep(time.Second)
}

第二個(gè)是在 for 循環(huán)范圍內(nèi)創(chuàng)建另一個(gè)變量:

package main

import (
	"fmt"
	"time"
)

func main() {

	ints := []int{1, 2, 3}
	for _, i := range ints {
		i := i
		go func() {
			fmt.Printf("%v\n", i)
		}()
	}

	time.Sleep(time.Second)
}

調(diào)用 i := i 可能看起來有點(diǎn)奇怪,但它完全有效。處于循環(huán)中意味著處于另一個(gè)范圍內(nèi)。所以 i := i 創(chuàng)建了另一個(gè)名為 i 的變量實(shí)例。當(dāng)然,為了便于閱讀,我們可能想用不同的名稱來稱呼它。

原文
https://itnext.io/the-top-10-most-common-mistakes-ive-seen-in-go-projects-4b79d4f6cd65

文章名稱:golang中經(jīng)常會(huì)犯的一些錯(cuò)誤
文章路徑:http://chinadenli.net/article0/dsoicoo.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供軟件開發(fā)、網(wǎng)站內(nèi)鏈、云服務(wù)器外貿(mào)建站、靜態(tài)網(wǎng)站、網(wǎng)站營銷

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來源: 創(chuàng)新互聯(lián)

成都seo排名網(wǎng)站優(yōu)化