溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

golang1.16中怎么內(nèi)嵌靜態(tài)資源

發(fā)布時(shí)間:2021-06-22 16:39:43 來(lái)源:億速云 閱讀:290 作者:Leah 欄目:編程語(yǔ)言

這篇文章給大家介紹golang1.16中怎么內(nèi)嵌靜態(tài)資源,內(nèi)容非常詳細(xì),感興趣的小伙伴們可以參考借鑒,希望對(duì)大家能有所幫助。

首先我們創(chuàng)建一個(gè)項(xiàng)目:

mkdir pk && cd pk

go mod init my.mod/pk

go get -u github.com/gobuffalo/packr/v2/... # 安裝庫(kù)

go get -u github.com/gobuffalo/packr/v2/packr2 # 安裝資源打包工具

然后我們復(fù)制一個(gè)png圖片和一個(gè)錄屏軟件制造的巨型gif文件進(jìn)images文件夾,整個(gè)項(xiàng)目看起來(lái)如下:

然后是我們的代碼:

package main

import (

  "fmt"

  "github.com/gobuffalo/packr/v2"

)

func main() {

  box := packr.New("myBox", "./images") // 創(chuàng)建內(nèi)嵌資源

  data, err := box.Find("screenrecord.gif") // 查找內(nèi)嵌資源

  if err != nil {

    log.Fatal(err)

  }

  fmt.Println(len(data))

}

想要完成資源嵌入,我們需要運(yùn)行packr2命令,之后直接運(yùn)行g(shù)o build即可,順利運(yùn)行后項(xiàng)目會(huì)是這樣:

packr的思路就是將資源文件編碼成合法的golang源文件,然后利用golang把這些代碼化的資源編譯進(jìn)程序里。這是比較主流的嵌入資源實(shí)現(xiàn)方案。

從上面的例子里我們可以看到這類方法有不少缺點(diǎn):

需要安裝額外的工具

會(huì)生成超大體積的生產(chǎn)代碼(是靜態(tài)資源的兩倍大,因?yàn)樾枰獙?duì)二進(jìn)制數(shù)據(jù)進(jìn)行一定的編碼才能正常存儲(chǔ)在go源文件里)

編譯完成的程序體積也是資源文件的兩倍多

程序加載時(shí)間長(zhǎng),上圖中程序運(yùn)行花費(fèi)了6秒,我們程序是存放在ssd上的,慢是因?yàn)閹?kù)需要對(duì)編碼的資源進(jìn)行處理

前兩點(diǎn)通過語(yǔ)言內(nèi)置工具或機(jī)制就可以得到解決,而對(duì)于后兩點(diǎn),靜態(tài)資源本身在計(jì)算機(jī)上也是二進(jìn)制存儲(chǔ)的,重復(fù)編碼解碼浪費(fèi)時(shí)間,如果可以直接把資源放進(jìn)程序里該多好。同時(shí)告別了生成代碼還可以讓我們的項(xiàng)目結(jié)構(gòu)更清晰。

所以,golang1.16的官方內(nèi)置版靜態(tài)資源嵌入方案誕生了。

準(zhǔn)備工作

golang的embed需要在1.16及之后的版本才能運(yùn)行,不過我們已經(jīng)可以自行編譯嘗鮮了(需要電腦已經(jīng)安裝了穩(wěn)定版本的golang):

mkdir -p ~/go-next && cd ~/go-next

git clone https://github.com/golang/go

cd go/src && bash ./make.bash

export GOROOT=~/go-next/go

alias newgo=${GOROOT}/bin/go

驗(yàn)證一下安裝:

$ newgo version

go version devel +256d729c0b Fri Oct 30 15:26:28 2020 +0000 linux/amd64

至此準(zhǔn)備工作就結(jié)束了。

如何匹配靜態(tài)資源

想要嵌入靜態(tài)資源,首先我們得利用embed這個(gè)新的標(biāo)準(zhǔn)庫(kù)。在聲明靜態(tài)資源的文件里我們需要引入這個(gè)庫(kù)。

對(duì)于我們想要嵌入進(jìn)程序的資源,需要使用//go:embed指令進(jìn)行聲明,注意//之后不能有空格。具體格式如下:

//go:embed pattern // pattern是path.Match所支持的路徑通配符 具體的通配符如下,如果你是在linux系統(tǒng)上,可以用man 7 glob查看更詳細(xì)的教程:

通配符 釋義

? 代表任意一個(gè)字符(不包括半角中括號(hào))

* 代表0至多個(gè)任意字符組成的字符串(不包括半角中括號(hào))

[...]和[!...] 代表任意一個(gè)匹配方括號(hào)里字符的字符,!表示任意不匹配方括號(hào)中字符的字符

[a-z]、[0-9] 代表匹配a-z任意一個(gè)字符的字符或是0-9中的任意一個(gè)數(shù)字

** 部分系統(tǒng)支持,*不能跨目錄匹配,**可以,不過目前個(gè)golang中和*是同義詞

我們可以在embed的pattern里自由組合這些通配符。

golang的embed默認(rèn)的根目錄從module的目錄開始,路徑開頭不可以帶/,不管windows還是其他系統(tǒng)路徑分割副一律使用/。如果匹配到的是目錄,那么目錄下的所有文件都會(huì)被嵌入(有部分文件夾和文件會(huì)被排除,后面詳細(xì)介紹),如果其中包含有子目錄,則對(duì)子目錄進(jìn)行遞歸嵌入。

下面舉一些例子,假設(shè)我們的項(xiàng)目在/tmp/proj:

//go:embed images

這是匹配所有位于/tmp/proj/images及其子目錄中的文件

//go:embed images/jpg/a.jpg

匹配/tmp/proj/images/jpg/a.jpg這一個(gè)文件

//go:embed a.txt

匹配/tmp/proj/a.txt

//go:embed images/jpg/*.jpg

匹配/tmp/proj/images/jpg下所有.jpg文件

//go:embed images/jpg/a?.jpg

匹配/tmp/proj/images/jpg下的a1.jpg a2.jpg ab.jpg等

//go:embed images/??g/*.*

匹配/tmp/proj/images下的jpg和png文件夾里的所有有后綴名的文件,例如png/123.png jpg/a.jpeg

//go:embed *

直接匹配整個(gè)/tmp/proj

//go:embed a.txt

//go:embed *.png *.jpg

//go:embed aa.jpg

可以指定多個(gè)//go:embed指令行,之間不能有空行,也可以用空格在一行里寫上對(duì)個(gè)模式匹配,表示匹配所有這些文件,相當(dāng)于并集操作

可以包含重復(fù)的文件或是模式串,golang對(duì)于相同的文件只會(huì)嵌入一次,很智能

另外,通配符的默認(rèn)目錄和源文件所在的目錄是同一目錄,所以我們只能匹配同目錄下的文件或目錄,不能匹配到父目錄。

舉個(gè)例子:

.

├── code

│   └── main.go

├── go.mod

├── imgs

│   ├── jpg

│   │   ├── a.jpg

│   │   ├── b.jpg

│   │   └── c.jpg

│   ├── png

│   │   ├── a.png

│   │   ├── b.png

│   │   └── c.png

│   └── screenrecord.gif

└── texts

    ├── en.txt

    ├── jp.txt

    └── zh.txt

5 directories, 12 files

考慮如上的目錄結(jié)構(gòu)。

在這里的main.go可見的資源只有code目錄及其子目錄里的文件,而imgs和texts里的文件是無(wú)法匹配到的。

如何使用嵌入的靜態(tài)資源 在了解了如何指定需要的靜態(tài)資源之后,我們?cè)搶W(xué)習(xí)如何使用它們了,還記得我們前面提到的embed標(biāo)準(zhǔn)庫(kù)嗎?

對(duì)于一個(gè)完整的嵌入資源,代碼中的聲明是這樣的:

//go:embed images

var imgs embed.FS

//go:embed a.txt

var txt []byte

//go:embed b.txt

var txt2 string

一共有三種數(shù)據(jù)格式可選:

數(shù)據(jù)類型 說明

[]byte 表示數(shù)據(jù)存儲(chǔ)為二進(jìn)制格式,如果只使用[]byte和string需要以import (_ "embed")的形式引入embed標(biāo)準(zhǔn)庫(kù)

string 表示數(shù)據(jù)被編碼成utf8編碼的字符串,因此不要用這個(gè)格式嵌入二進(jìn)制文件比如圖片,引入embed的規(guī)則同[]byte

embed.FS 表示存儲(chǔ)多個(gè)文件和目錄的結(jié)構(gòu),[]byte和string只能存儲(chǔ)單個(gè)文件

實(shí)際上接受嵌入文件數(shù)據(jù)的變量也可以是string和[]byte的類型別名或基于他們定義的新類型,例如下面的代碼那樣:

type StringAlias = string

//go:embed a.txt

var text1 StringAlias

type NewBytes []byte

//go:embed b.txt

var text2 NewBytes

這一變化是issue 43602中提出的,并在commit ec94701中實(shí)現(xiàn)。

下面我們看個(gè)更具體例子,目錄結(jié)構(gòu)如下:

$ tree -sh .

.

├── [ 487]  embed_fs.go

├── [ 235]  embed_img.go

├── [ 187]  embed_img2.go

├── [ 513]  embed_img_fs.go

├── [ 211]  embed_text.go

├── [ 660]  embed_text_fs.go

├── [  30]  go.mod

├── [   0]  imgs

│   ├── [   0]  jpg

│   │   ├── [606K]  a.jpg

│   │   ├── [976K]  b.jpg

│   │   └── [342K]  c.jpg

│   ├── [   0]  png

│   │   ├── [4.7M]  a.png

│   │   ├── [1.4M]  b.png

│   │   └── [1.7M]  c.png

│   └── [ 77M]  screenrecord.gif

├── [ 98K]  macbeth.txt

└── [   0]  texts

    ├── [  12]  en.txt

    ├── [  25]  jp.txt

    └── [  16]  zh.txt

4 directories, 18 files

目錄包含了一些靜態(tài)圖片,一個(gè)錄屏文件,一個(gè)莎士比亞的麥克白劇本。當(dāng)然還有我們的測(cè)試代碼。

處理單個(gè)文件

我們先來(lái)看用[]byte和string嵌入單個(gè)文件的例子:

package main

import (

    "fmt"

    _ "embed"

)

//go:embed macbeth.txt

var macbeth string

//go:embed texts/en.txt

var hello string

func main() {

    fmt.Println(len(macbeth)) // 麥克白的總字符數(shù)

    fmt.Println(hello) // Output: Hello, world

}

如你所見,聲明嵌入內(nèi)容的變量一定要求使用var聲明。我們直接用newgo run embed_txt.go或go build embed_txt.go && ./embed_txt即可完成編譯運(yùn)行,過程中不會(huì)生成任何中間代碼。另外變量是否是公開的(首字母是否大小寫)并不會(huì)對(duì)資源的嵌入產(chǎn)生影響。

在issue 43216中,基于如下的矛盾golang取消了對(duì)本地作用域變量的嵌入資源聲明的支持:

如果嵌入資源只初始化一次,那么每次函數(shù)調(diào)用都將共享這些資源,考慮到任何函數(shù)都可以作為goroutine運(yùn)行,這會(huì)帶來(lái)嚴(yán)重的潛在風(fēng)險(xiǎn);

如果每次函數(shù)調(diào)用時(shí)都重新初始化,這樣做會(huì)產(chǎn)生昂貴的性能開銷。

因此最后golang官方在commit 54198b0中關(guān)閉了本地作用域的靜態(tài)資源嵌入功能?,F(xiàn)在你的代碼應(yīng)該這樣寫:

+ //go:embed hello.txt

+ var hello string

func Print() {

-   //go:embed hello.txt

-   var hello string

+   embedString := hello

    ....

}

再來(lái)看看二進(jìn)制文件的例子,embed_img.go如下所示:

package main

import (

    "fmt"

    _ "embed"

)

//go:embed imgs/screenrecord.gif

var gif []byte

//go:embed imgs/png/a.png

var png []byte

func main() {

    fmt.Println("gif size:", len(gif)) // gif size: 81100466

    fmt.Println("png size:", len(png)) // png size: 4958264

}

如果編譯運(yùn)行這個(gè)程序,你會(huì)發(fā)現(xiàn)二進(jìn)制文件的大小是89M(不同系統(tǒng)會(huì)有差異),比我們之前使用packr創(chuàng)建的要小了許多。

處理多個(gè)文件和目錄

下面就要進(jìn)入本文的重頭戲了,新的標(biāo)準(zhǔn)庫(kù)embed的使用。

如果你newgo doc embed的話會(huì)發(fā)現(xiàn)整個(gè)標(biāo)準(zhǔn)庫(kù)里只有一個(gè)FS類型(之前按提案被命名為Files,后來(lái)考慮到用目錄結(jié)構(gòu)組織多個(gè)資源更類似新的io/fs.FS接口,故改名),而我們對(duì)靜態(tài)資源的操作也全都依賴這個(gè)FS。下面接著用例子說明:

package main

import (

    "fmt"

    "embed"

)

//go:embed texts

var dir embed.FS

// 兩者沒什么區(qū)別

//go:embed texts/*

var files embed.FS

func main(){

    zh, err := files.ReadFile("texts/zh.txt")

    if err != nil {

        fmt.Println("read zh.txt error:", err)

    } else {

        fmt.Println("zh.txt:", string(zh))

    }

    jp, err := dir.ReadFile("jp.txt")

    if err != nil {

        fmt.Println("read  jp.txt error:", err)

    } else {

        fmt.Println("jp.txt:", string(jp))

    }

    jp, err = dir.ReadFile("texts/jp.txt")

    if err != nil {

        fmt.Println("read  jp.txt error:", err)

    } else {

        fmt.Println("jp.txt:", string(jp))

    }

}

運(yùn)行結(jié)果:

zh.txt: 你好,世界

read  jp.txt error: open jp.txt: file does not exist

jp.txt: こんにちは、世界

我們想讀取單個(gè)文件需要用ReadFile方法,它接受一個(gè)path字符串做參數(shù),從中查找對(duì)應(yīng)的文件然后返回([]byte, error)。

要注意的是文件路徑必須要明確寫出自己的父級(jí)目錄,否則會(huì)報(bào)錯(cuò),因?yàn)榍度胭Y源是按它存儲(chǔ)路徑相同的結(jié)構(gòu)存儲(chǔ)的,和通配符怎么指定無(wú)關(guān)。

Open是和ReadFile類似的方法,只不過返回了一個(gè)fs.File類型的io.Reader,因此這里就不再贅述,需要使用Open還是ReadFile可以由開發(fā)者根據(jù)自身需求決定。

embed.FS自身是只讀的,所以我們不能在運(yùn)行時(shí)添加或刪除嵌入的文件,fs.File也是只讀的,所以我們不能修改嵌入資源的內(nèi)容。

如果只是提供了一個(gè)查找讀取資源的能力,那未免小看了embed。在golang1.16里任意實(shí)現(xiàn)了io/fs.FS接口的類型都可以表現(xiàn)的像是真實(shí)存在于文件系統(tǒng)中的目錄一樣,哪怕它其實(shí)是在內(nèi)存里的類map數(shù)據(jù)結(jié)構(gòu)。因此我們也可以像遍歷目錄一樣去處理embed.FS:

package main

import (

 "embed"

 "fmt"

)

// 更推薦直接用imgs去匹配

//go:embed imgs/**

var dir embed.FS

// 遍歷當(dāng)前目錄,有興趣你可以改成遞歸版本的

func printDir(name string) {

 // 返回[]fs.DirEntry

 entries, err := dir.ReadDir(name)

 if err != nil {

  panic(err)

 }

 fmt.Println("dir:", name)

 for _, entry := range entries {

  // fs.DirEntry的Info接口會(huì)返回fs.FileInfo,這東西被從os移動(dòng)到了io/fs,接口本身沒有變化

  info, _ := entry.Info()

  fmt.Println("file name:", entry.Name(), "\tisDir:", entry.IsDir(), "\tsize:", info.Size())

 }

 fmt.Println()

}

func main() {

 printDir("imgs")

 printDir("imgs/jpg")

 printDir("imgs/png")

}

運(yùn)行結(jié)果:

dir: imgs

file name: jpg  isDir: true     size: 0

file name: png  isDir: true     size: 0

file name: screenrecord.gif     isDir: false    size: 81100466

dir: imgs/jpg

file name: a.jpg        isDir: false    size: 620419

file name: b.jpg        isDir: false    size: 999162

file name: c.jpg        isDir: false    size: 349725

dir: imgs/png

file name: a.png        isDir: false    size: 4958264

file name: b.png        isDir: false    size: 1498303

file name: c.png        isDir: false    size: 1751934

唯一和真實(shí)的目錄不一樣的地方是目錄文件的大小,在ext4等文件系統(tǒng)上目錄會(huì)存儲(chǔ)子項(xiàng)目的元信息,所以大小通常不為0。

如果想要內(nèi)嵌整個(gè)module,則在引用的時(shí)候需要使用"."這個(gè)名字,但除了單獨(dú)使用之外路徑里不可以包含..或者.,換而言之,embed.FS不支持相對(duì)路徑,把上面的代碼稍加修改:

package main

import (

    "fmt"

    "embed"

)

//go:embed *

var dir embed.FS

func main() {

    printDir(".")

    //printDir("./texts/../imgs") panic: open ./texts/../imgs: file does not exist

}

程序輸出:

dir: .

file name: embed_fs.go  isDir: false    size: 484

file name: embed_img.go         isDir: false    size: 235

file name: embed_img2.go        isDir: false    size: 187

file name: embed_img_fs.go      isDir: false    size: 692

file name: embed_text.go        isDir: false    size: 211

file name: embed_text_fs.go     isDir: false    size: 603

file name: go.mod       isDir: false    size: 30

file name: imgs         isDir: true     size: 0

file name: macbeth.txt  isDir: false    size: 100095

file name: texts        isDir: true     size: 0

因?yàn)槭褂昧隋e(cuò)誤的文件名或路徑會(huì)在運(yùn)行時(shí)panic,所以要格外小心。(當(dāng)然//go:embed是在編譯時(shí)檢查的,而且同樣不支持相對(duì)路徑,同時(shí)也不支持超出了module目錄的任何路徑,比如go module在/tmp/proj,我們指定了/tmp/proj2)

你也可以用embed.FS處理單個(gè)文件,但我個(gè)人認(rèn)為單個(gè)文件就沒必要再多包裝一層了。

由于是golang內(nèi)建的支持,所以上述的代碼無(wú)需調(diào)用任何第三方工具,也沒有煩人的生成代碼,不得不說golang對(duì)工程控制的把握上還是相當(dāng)可靠的。

一些陷阱

方便的功能背后往往也會(huì)有陷阱相隨,golang的內(nèi)置靜態(tài)資源嵌入也不例外。

隱藏文件的處理 根據(jù)2020年11月21日的issue,現(xiàn)在golang在對(duì)目錄進(jìn)行遞歸嵌入的時(shí)候會(huì)忽略名字以下劃線(_)和點(diǎn)(.)開頭的文件或目錄。這些文件名在部分文件系統(tǒng)中為隱藏文件,issue的提出者認(rèn)為默認(rèn)不應(yīng)該包含這些文件,隱藏文件通常包含對(duì)程序來(lái)說沒有意義的元數(shù)據(jù),或是用戶的隱私配置,除非明確聲明,否則嵌入資源中包含隱藏文件是不妥的。

舉個(gè)例子,假設(shè)我們有個(gè)images文件夾,底下有a.jpg,.b.jpg兩個(gè)常規(guī)文件,以及_photo_metadata和pngs兩個(gè)子目錄,根據(jù)最新的commit,以下的嵌入資源指令的效果如注釋中的解釋:

//go:embed images var images embed.FS // 不包含.b.jpg和_photo_metadata目錄

//go:embed images/* var images embed.FS // 注意!?。∵@里包含.b.jpg和_photo_metadata目錄

//go:embed images/.b.jog var bJPG []byte // 明確給出文件名也不會(huì)被忽略 注意第二條。使用*相當(dāng)于明確給出了目錄下所有文件的名字,因此點(diǎn)和下劃線開頭的文件和目錄也會(huì)被包含。

當(dāng)然,隱藏文件不止文件名特殊這么簡(jiǎn)單,在部分文件系統(tǒng)上擁有正常文件名的文件通過增加某些flag或者attribute也可以變?yōu)殡[藏,目前怎么處理此類情況還沒有定論。官方暫且按照社區(qū)的習(xí)慣使用文件名進(jìn)行區(qū)分。

另外對(duì)于*是否應(yīng)該包含隱藏文件的爭(zhēng)論也沒有停止,官方暫且認(rèn)為應(yīng)該包含隱藏文件,這點(diǎn)要多加注意。

資源是否應(yīng)該被壓縮

靜態(tài)資源嵌入的提案被接受后爭(zhēng)論最多的就是是否應(yīng)該對(duì)資源采取壓縮,壓縮后的資源更緊湊,不會(huì)浪費(fèi)太多存儲(chǔ)空間,特別是一些大文本文件。同時(shí)更大的程序運(yùn)行加載時(shí)間越長(zhǎng),cpu緩存利用率可能會(huì)變低。

而反對(duì)意見認(rèn)為壓縮和運(yùn)行時(shí)的解壓一個(gè)浪費(fèi)編譯的時(shí)間一個(gè)浪費(fèi)運(yùn)行時(shí)的效率,在用戶沒有明確指定的情況下用戶需要為自己不需要的功能花費(fèi)代價(jià)。

目前官方采用的實(shí)現(xiàn)是不壓縮嵌入資源,并預(yù)計(jì)在后續(xù)版本加入控制是否啟用壓縮的選項(xiàng)。

而真正的陷阱是接下來(lái)的內(nèi)容。

潛在的嵌入資源副本

前文中提到過重復(fù)的匹配和相同的文件golang會(huì)自動(dòng)只保留一份在變量中。沒錯(cuò),然而這是針對(duì)同一個(gè)變量的多個(gè)匹配說的,如果考慮下面的代碼:

package main

import (

 _ "embed"

 "fmt"

)

//go:embed imgs/screenrecord.gif

var b []byte

//go:embed imgs/screenrecord.gif

var a []byte

func main() {

 fmt.Printf("a: %p %d\n", &a, len(a))

 fmt.Printf("b: %p %d\n", &b, len(b))

}

猜猜輸出是什么:

a: 0x9ff5a50 81100466

b: 0x9ff5a70 81100466

a和b的地址不一樣!那也沒關(guān)系,我們知道slice是引用類型,底層說不定引用了同一個(gè)數(shù)組呢?那再來(lái)看看文件大?。?/p>

tree -sh .

.

├── [ 484]  embed_fs.go

├── [ 230]  embed_img2.go

├── [157M]  embed_img2

├── ...

├── [   0]  imgs

│   ├ ...

│   └── [ 77M]  screenrecord.gif

├── ...

4 directories, 19 files

程序是資源的兩倍大,這差不多就可以說明問題了,資源被復(fù)制了一份。不過從代碼的角度來(lái)考慮,a和b是兩個(gè)不同的對(duì)象,所以引用不同的數(shù)據(jù)也說的過去,但在開發(fā)的時(shí)候一定要小心,不要讓兩個(gè)資源集合出現(xiàn)交集,否則就要付出高昂的存儲(chǔ)空間代價(jià)了。

過大的可執(zhí)行文件帶來(lái)的性能影響 程序文件過大會(huì)導(dǎo)致初次運(yùn)行加載時(shí)間的增長(zhǎng),這是眾所周知的。

然而過大的程序文件還可能會(huì)降低運(yùn)行效率。程序需要利用現(xiàn)代的cpu快速緩存體系來(lái)提高性能,而更大的二進(jìn)制文件意味著對(duì)于反復(fù)運(yùn)行的熱點(diǎn)功能cpu的快速緩存很可能會(huì)面臨更多的緩存失效,因?yàn)榫彺娴拇笮∮邢?,需要兩次三次的讀取和刷新才能運(yùn)行完一個(gè)熱點(diǎn)代碼片段。這就是為什么幾乎所有的編譯器都會(huì)自行指定函數(shù)是否會(huì)被內(nèi)聯(lián)化而不是把這種控制權(quán)利移交給用戶的原因。

然而嵌入靜態(tài)文件之后究竟會(huì)對(duì)性能有多少影響呢?目前缺乏實(shí)驗(yàn)證據(jù),所以沒有定論。

通過修改二進(jìn)制文件的一部分格式也可以讓代碼部分和資源部分分離從而代碼在cpu看來(lái)更加緊湊,當(dāng)然這么做會(huì)不會(huì)嚴(yán)重破壞兼容,是否真的有用也未可知。

關(guān)于golang1.16中怎么內(nèi)嵌靜態(tài)資源就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,可以學(xué)到更多知識(shí)。如果覺得文章不錯(cuò),可以把它分享出去讓更多的人看到。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI