1. 程式人生 > >GO 語言五步讓你成為高手

GO 語言五步讓你成為高手



這裡是GO程式設計師的五個進化階段:

第一個階段(菜逼): 剛剛學習了這門語言。 已經通過一些教程或者培訓班瞭解基本的語法,可以寫短的程式碼片段。

第二個階段 (探索者): 可以寫一個完整的程式,但不懂一些更高階的語言特徵,比如“channels”。還沒有使用GO寫一個大專案。

第三個階段(大手): 你能熟練的使用Go, 能夠用GO去解決,生產環境中一個具體和完整的問題。已經形成了一套自己的慣用法和常用程式碼庫。在你的編碼方案中Go是一個非常好用的工具。

第四階段 (大神): 絕逼清楚Go語言的設計選擇和背後的動機。能理解的簡潔和可組合性哲學。

佈道師: 積極地與他人分享關於Go語言知識和你對Go語言的理解。在各種合適的場所發出自己的聲音, 參與郵件列表、建立QQ群、做專題報告。成為一個佈道者不見得是一個完全獨立的階段,這個角色可以在上述的任何一個階段中。

第一階段: 菜逼

菜鳥在這個階段使用Go去建立一些小專案或者玩具專案。他們應該會利用到Go tour, Go playground, Go文件, 和郵件列表(golang-nuts).

func main() {
    fmt.Println(stringutil.Reverse("!selpmaxe oG ,olleH"))}

這是Go語言寫的簡單例子,這個程式碼段來自 hello.go 。 點選就可以檢視完整程式碼擼。

一項重要的技能,新人應該試著學習如何正確提問。很多新人在郵件列表裡面這樣說“嘿,這報錯了”,這並沒有提供足夠的資訊,讓別人能理解並幫助他們解決問題。別人看到的是一個貼上了幾百行的程式碼的帖子,並沒有花費精力來重點說明所遇到的問題。

所以, 應該儘量避免直接貼上程式碼到論壇。而應該使用可以編輯並且可以在瀏覽器中直接執行的Go playground的“分享”按鈕連結到程式碼片段。

Phase 2: the explorer

探索者已經可以使用Go寫一些小的軟體,但有時仍然會有些迷茫。他們可能不完全明白怎麼使用Go的高階特性,比如通道。雖然他們還有很多東西要學習,但已掌握的足夠做一些有用的事情了!他們開始對Go的潛能有感覺了,並對它們能使用Go建立的東西感到興奮。

在探索階段通常會經歷兩個步驟。第一,膨脹的預期達到頂點,你覺得可以用Go做所有的事情,但還並不能明白或領悟到Go的真諦。你大概會用所熟悉的語言的模式和慣用語來寫Go程式碼,但對於什麼是地道的Go,還沒有比較強烈的感覺。你開始嘗試著手幹這樣的事情--“遷移架構X,從Y語言到Go語言”。

到達預期膨脹的頂點之後,你會遇到理想幻滅的低谷。你開始想念語言Y的特性X,此時你還沒有完全的掌握地道的Go。你還在用其他程式語言的風格來寫Go語言的程式,你甚至開始覺得沮喪。你可能在大量使用reflect和unsafe這兩個包,但這不是地道的Go。地道的Go不會使用那些魔法一樣的東西。

這個探索階段產生的專案的一個很好的例子就是Martini Web框架。Martini是一個Go語言的早期Web框架,它從Ruby的Web框架當中吸收了很多思想(比如依賴注入)。最初,這個框架在社群中引起了強烈的反響,但是它逐漸在效能和可除錯性上受到了一些批評。Martini框架的作者Jeremy Saenz積極響應這些來自Go社群的反饋,寫了一個更加符合Go語言規範的庫Negroni

func (m *Martini) RunOnAddr(addr string) {
    // TODO: Should probably be implemented using a new instance of http.Server in place of
    // calling http.ListenAndServer directly, so that it could be stored in the martini struct for later use.
    // This would also allow to improve testing when a custom host and port are passed.
 
    logger := m.Injector.Get(reflect.TypeOf(m.logger)).Interface().(*log.Logger)
    logger.Printf("listening on %s (%s)\n", addr, Env)
    logger.Fatalln(http.ListenAndServe(addr, m))}

來自Martini框架的互動式程式碼片段,它是不地道的Go的例子。注意用反射包實現的依賴注入

func TestNegroniServeHTTP(t *testing.T) {
    result := ""
    response := httptest.NewRecorder()
 
    n := New()
    n.Use(HandlerFunc(func(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
        result += "foo"
        next(rw, r)
        result += "ban"
    }))
    n.Use(HandlerFunc(func(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
        result += "bar"
        next(rw, r)
        result += "baz"
    }))
    n.Use(HandlerFunc(func(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
        result += "bat"
        rw.WriteHeader(http.StatusBadRequest)
    }))
 
    n.ServeHTTP(response, (*http.Request)(nil))
 
    expect(t, result, "foobarbatbazban")
    expect(t, response.Code, http.StatusBadRequest)}

來自Negroni庫的互動式程式碼片段,它是地道的Go的例子

其他語言在提供一些核心功能,比如HTTP處理的時候,往往需要依賴第三方庫。但是Go語言在這一點上很不同,它的標準庫非常強大。如果你認為Go標準庫沒有強大到可以做你想做的事情,那麼我說你錯了。Go語言標準庫難以置信的強大,值得你花時間閱讀它的程式碼,學習它實現的模式。

func (srv *Server) ListenAndServe() error {
    addr := srv.Addr
    if addr == "" {
        addr = ":http"
    }
    ln, err := net.Listen("tcp", addr)
    if err != nil {
        return err
    }
    return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})}

Go標準庫中的ListenAndServe函式片段。如果你寫過Go程式,你可能已經呼叫過這個函式很多次了,但是你曾經花時間看過它的實現麼?去點選上面的程式碼片段吧。

幻滅的低谷中的幻滅感來自於這樣的事實:你還在用其他語言的模式來想問題,而且你還沒有完全探索過Go能提供給你什麼。下面是一些好玩的事情,你可以做一下來打破困境,進一步探索這門語言中好玩的事。

go generate

現在來看看go generate。go generate是一個你可以用來自動自成Go程式碼的命令。你可以結合例如jsonenums(一個用於為列舉型別自動生成JSON編組樣板程式碼的類庫)這樣的超程式設計來使用go generate快速自動實現重複乏味程式碼的編寫。在Go標準類庫裡面已經有大量可以用於解析AST的介面,而AST使得編寫超程式設計工具更簡單,更容易。在會議上,有另外兩次討論(Go語言中的超程式設計實踐擁抱標準類庫)談及到了這一點。

func main() {
    flag.Parse()
    if len(*typeNames) == 0 {
        log.Fatalf("the flag -type must be set")
    }
    types := strings.Split(*typeNames, ",")
 
    // Only one directory at a time can be processed, and the default is ".".
    dir := "."
    if args := flag.Args(); len(args) == 1 {
        dir = args[0]
    } else if len(args) > 1 {
        log.Fatalf("only one directory at a time")
    }
 
    pkg, err := parser.ParsePackage(dir, *outputSuffix+".go")
    if err != nil {
        log.Fatalf("parsing package: %v", err)
    }
 
    var analysis = struct {
        Command        string
        PackageName    string
        TypesAndValues map[string][]string
    }{
        Command:        strings.Join(os.Args[1:], " "),
        PackageName:    pkg.Name,
        TypesAndValues: make(map[string][]string),
    }
 
    // Run generate for each type.
    for _, typeName := range types {
        values, err := pkg.ValuesOfType(typeName)
        if err != nil {
            log.Fatalf("finding values for type %v: %v", typeName, err)
        }
        analysis.TypesAndValues[typeName] = values
 
        var buf bytes.Buffer
        if err := generatedTmpl.Execute(&buf, analysis); err != nil {
            log.Fatalf("generating code: %v", err)
        }
 
        src, err := format.Source(buf.Bytes())
        if err != nil {
            // Should never happen, but can arise when developing this code.
            // The user can compile the output to see the error.
            log.Printf("warning: internal error: invalid Go generated: %s", err)
            log.Printf("warning: compile the package to analyze the error")
            src = buf.Bytes()
        }
 
        output := strings.ToLower(typeName + *outputSuffix + ".go")
        outputPath := filepath.Join(dir, output)
        if err := ioutil.WriteFile(outputPath, src, 0644); err != nil {
            log.Fatalf("writing output: %s", err)
        }
    }}

一段互動的片段演示瞭如何編寫jsonenums命令。

OpenGL

許多人使用Go作web服務,但是你知道你也可以用Go寫出很cool的圖形應用嗎?檢視Go在OpenGL中的捆綁

func main() {
    glfw.SetErrorCallback(errorCallback)
 
    if !glfw.Init() {
        panic("Can't init glfw!")
    }
    defer glfw.Terminate()
 
    window, err := glfw.CreateWindow(Width, Height, Title, nil, nil)
    if err != nil {
        panic(err)
    }
 
    window.MakeContextCurrent()
 
    glfw.SwapInterval(1)
 
    gl.Init()
 
    if err := initScene(); err != nil {
        fmt.Fprintf(os.Stderr, "init: %s\n", err)
        return
    }
    defer destroyScene()
 
    for !window.ShouldClose() {
        drawScene()
        window.SwapBuffers()
        glfw.PollEvents()
    }}

互動式的片段正說明Go的OpenGL捆綁能製作Gopher cube。點選函式或方法名去探索。

黑客馬拉松和挑戰

你也可以觀看挑戰和黑客馬拉松,類似Gopher GalaGo Challenge。在過去,來自世界各地的程式設計師一起挑戰一些真實的酷專案,你可以從中獲取靈感。

第三階段: 老手

作為一個老手,這意味著你可以解決很多Go語言中你關心的問題。新的需要解決的問題會帶來新的疑問,經過試錯,你學會了在這門語言中什麼是可以做的,什麼是不能做的。此時,你已經對這門語言的習慣和模式有了一個堅實的理解。你可以非常高效地工作,寫出可讀,文件完善,可維護的程式碼。

成為老手的一個很好的方法就是在大專案上工作。如果你自己有一個專案的想法,開始動手去做吧(當然你要確定它並不是已經存在了)。大多數人也許並沒有一個很大的專案的想法,所以他們可以對已經存在的專案做出貢獻。Go語言已經有很多大型專案,而且它們正在被廣泛使用,比如Docker, Kubernetes和Go本身。可以看看這個專案列表

func (cli *DockerCli) CmdRestart(args ...string) error {
    cmd := cli.Subcmd("restart", "CONTAINER [CONTAINER...]", "Restart a running container", true)
    nSeconds := cmd.Int([]string{"t", "-time"}, 10, "Seconds to wait for stop before killing the container.")
    cmd.Require(flag.Min, 1)
 
    utils.ParseFlags(cmd, args, true)
 
    v := url.Values{}
    v.Set("t", strconv.Itoa(*nSeconds))
 
    var encounteredError error
    for _, name := range cmd.Args() {
        _, _, err := readBody(cli.call("POST", "/containers/"+name+"/restart?"+v.Encode(), nil, false))
        if err != nil {
            fmt.Fprintf(cli.err, "%s\n", err)
            encounteredError = fmt.Errorf("Error: failed to restart one or more containers")
        } else {
            fmt.Fprintf(cli.out, "%s\n", name)
        }
    }
    return encounteredError}

Docker專案的互動式程式碼片段。點選函式名,開始探索之旅吧。

老手應該對Go生態系統的工具有一個很強的掌握,因為這些工具真的提高生產效率。你應該瞭解go generate,go vet,go test-race, 和gofmt/goimports/goreturns。你應該使用go fmt,因為它會自動把你的程式碼按照Go社群的風格標準來格式化。goimports可以做同樣的事情,而且還會新增丟失的imports。goretures不光做了前面所說的事情,還可以在返回表示式新增丟失的錯誤,這是大家都討厭的地方。

在老手階段,你一定要開始做code review。code review的意義並不是要修改或者找到錯誤(那是測試人員做的事情)。code review可以幫助維持統一的程式設計風格,提高軟體的總體質量,還可以在別人的反饋中提高你自己的程式設計技術。幾乎所有的大型開源專案都對每一個提交做code review。

下面是一個從人類的反饋當中學習的例子:Google的Go團隊以前都在main函式的外面宣告命令列標記。在去年的GopherCon會議上,Francesc遇到了SoundCloud公司的Peter Bourgon(@peterbourgon)。Peter Bourgon說在SoundCloud,他們都在main函式內部宣告標記,這樣他們不會錯誤地在外部使用標記。Francesc現在認為這是最佳實踐。

第四階段:專家

作為一個專家,你很好地瞭解了語言的哲學思想。對於Go語言的特性,你知道何時應該使用,何時不應該使用。例如,Jeremy Saenz在dotGo風暴討論中談論到了何時不該使用介面。

func (client *Client) Go(serviceMethod string, args interface{}, reply interface{}, done chan *Call) *Call {
    call := new(Call)
    call.ServiceMethod = serviceMethod
    call.Args = args
    call.Reply = reply
    if done == nil {
        done = make(chan *Call, 10) // buffered.
    } else {
        // If caller passes done != nil, it must arrange that
        // done has enough buffer for the number of simultaneous
        // RPCs that will be using that channel.  If the channel
        // is totally unbuffered, it's best not to run at all.
        if cap(done) == 0 {
            log.Panic("rpc: done channel is unbuffered")
        }
    }
    call.Done = done
    client.send(call)
    return call}

來自標準類庫的一小塊互動程式碼片段使用了頻道。理解標準類庫裡面的模式背後的決策原因是成為一個專家必經之路。

但是不要成為只侷限於單一語言的專家。跟其他任何語言一樣,Go僅僅只是一個工具。你還應該去探索其他語言,並且學習他們的模式和風格。Francesc從他使用Go的經驗中找到了編寫JavaScript的啟發。他還喜歡重點關注於不可變性和致力於避免易變性的Haskell語言,並從中獲得瞭如何編寫Go程式碼的靈感。

佈道者

作為一個佈道者,你分享自己的知識,傳授你學會的和你提出的最佳實踐。你可以分享自己對Go喜歡或者不喜歡的地方。全世界各地都有Go會議,找到離你最近的

你可以在任何一個階段成為佈道者,不要等到你成為這個領域的專家的時候才發出自己的聲音。在你學習Go的任何一個階段,提出問題,結合你的經驗給出反饋,不要羞於提出自己不喜歡的地方。你提出的反饋可以幫助社群改善做事情的方法,也可能改變你自己對程式設計的看法。

func main() {
    httpAddr := flag.String("http", "127.0.0.1:3999", "HTTP service address (e.g., '127.0.0.1:3999')")
    originHost := flag.String("orighost", "", "host component of web origin URL (e.g., 'localhost')")
    flag.StringVar(&basePath, "base", "", "base path for slide template and static resources")
    flag.BoolVar(&present.PlayEnabled, "play", true, "enable playground (permit execution of arbitrary user code)")
    nativeClient := flag.Bool("nacl", false, "use Native Client environment playground (prevents non-Go code execution)")
    flag.Parse()
 
    if basePath == "" {
        p, err := build.Default.Import(basePkg, "", build.FindOnly)
        if err != nil {
            fmt.Fprintf(os.Stderr, "Couldn't find gopresent files: %v\n", err)
            fmt.Fprintf(os.Stderr, basePathMessage, basePkg)
            os.Exit(1)
        }
        basePath = p.Dir
    }
    err := initTemplates(basePath)
    if err != nil {
        log.Fatalf("Failed to parse templates: %v", err)
    }
 
    ln, err := net.Listen("tcp", *httpAddr)
    if err != nil {
        log.Fatal(err)
    }
    defer ln.Close()
 
    _, port, err := net.SplitHostPort(ln.Addr().String())
    if err != nil {
        log.Fatal(err)
    }
    origin := &url.URL{Scheme: "http"}
    if *originHost != "" {
        origin.Host = net.JoinHostPort(*originHost, port)
    } else if ln.Addr().(*net.TCPAddr).IP.IsUnspecified() {
        name, _ := os.Hostname()
        origin.Host = net.JoinHostPort(name, port)
    } else {
        reqHost, reqPort, err := net.SplitHostPort(*httpAddr)
        if err != nil {
            log.Fatal(err)
        }
        if reqPort == "0" {
            origin.Host = net.JoinHostPort(reqHost, port)
        } else {
            origin.Host = *httpAddr
        }
    }
 
    if present.PlayEnabled {
        if *nativeClient {
            socket.RunScripts = false
            socket.Environ = func() []string {
                if runtime.GOARCH == "amd64" {
                    return environ("GOOS=nacl", "GOARCH=amd64p32")
                }
                return environ("GOOS=nacl")
            }
        }
        playScript(basePath, "SocketTransport")
        http.Handle("/socket", socket.NewHandler(origin))
    }
    http.Handle("/static/", http.FileServer(http.Dir(basePath)))
 
    if !ln.Addr().(*net.TCPAddr).IP.IsLoopback() &&
        present.PlayEnabled && !*nativeClient {
        log.Print(localhostWarning)
    }
 
    log.Printf("Open your web browser and visit %s", origin.String())
    log.Fatal(http.Serve(ln, nil))

流行的present命令的main函式,很多Go的使用者使用它來製作幻燈片。許多演講者修改了這個模組來滿足自己的需要。

相關推薦

GO 語言成為高手

 這裡是GO程式設計師的五個進化階段: 第一個階段(菜逼): 剛剛學習了這門語言。 已經通過一些教程或者培訓班瞭解基本的語法,可以寫短的程式碼片段。 第二個階段 (探索者): 可以寫一個完整的程式,但不懂一些更高階的語言特徵,比如“channels”。還沒有使用GO寫一

成為更優秀的程序員

程序 信號 round 計算 做到 努力 解決問題 部署 true 1. 永遠不要復制代碼不惜任何代價避免重復的代碼。如果一個常用的代碼片段出現在了程序中的幾個不同地方,重構它,把它放到一個自己的函數裏。重復的代碼會導致你的同事在讀你的代碼時產生困惑。而重復的代碼如果在一個

成為Oracle DBA

隨著Oracle技術和市場的快速發展,目前從事Oracle DBA工作的人群越來越龐大,從一些我經常去的論壇和社群中大家發的帖子上看的出,很多人都渴望在現在或將來從事這一領域的工作,現在的市場需求也很大,但不難看出,各個公司對DBA這個崗位的要求也越來越高,當然了,高薪、高挑戰和可持續發展的

Vue.js最佳實踐(成為Vue.js大師)

(點選上方公眾號,可快速關注)作者:zach5078segmentfault.com/a/119

42進階學習—成為優秀的Java大數據科學家!

燈塔 融合 pytho 數據庫管理 學習 網絡 深入 非關系型 模塊 作者 燈塔大數據 本文轉自公眾號燈塔大數據(DTbigdata),轉載需授權 如果你對各種數據類的科學課題感興趣,你就來對地方了。本文將給大家介紹讓你成為優秀數據科學家的42個步驟。深入掌握數據準備,機

最全信用卡養卡攻略,成為用卡高手

在用卡過程中,難免會有人犯一些小錯誤,導致臨時額度提不上去,或者被銀行警告惡意套現等。怎麼養好卡呢?   在用卡過程中,難免會有人犯一些小錯誤,導致臨時額度提不上去,或者被銀行警告惡意套現等。   怎麼養好卡呢?小編今天來教你幾招~   什麼叫養卡?   一般的養卡是指多刷卡多消費,並且及時還款,

Ubuntu完全教程,成為Ubuntu高手

Ubuntu的發音 Ubuntu,源於非洲祖魯人和科薩人的語言,發作 oo-boon-too 的音。瞭解發音是有意義的,您不是第一個為此困惑的人,當然,也不會是最後一個:) 大多數的美國人讀 ubuntu 時,將 u 作為母音發音,類似單詞 who 或者 boo ,重音在第二個音節即 u'buntu

IT運維管理必備工具大全,成為真正的高手

統一帳號管理 你還在自己寫指令碼批量增加機器的使用者名稱、分組和修改密碼或者同步主機的/etc/passwd嗎?你還在使用指令碼批量對使用者設定許可權嗎?如果有一臺帳號主機能夠提供所有伺服器的帳號、密碼、許可權控制,如此一來,如果想要增加、修改、刪除使用者,只要到這臺

這10本由淺入深的好書,或成為機器學習領域的專家

微博 .com 比較 編碼風格 兩個 行數據 開始 自己 推薦書 [email protected]/* */ 老師推薦,阿裏雲雲棲社區組織翻譯。 以下為譯文: 機器學習是個跨領域的學科,而且在實際應用中有巨大作用,但是沒有一本書能讓你成為機器學習

快速學習C語言途徑,少走彎路

串處理 毫無 深入 中國人 sql 以及 ubi 思維 思路   1.標準C語言能幹什麽?   坦白講,在今天軟件已經發展了半個多世紀,單純的C語言什麽都幹不了。標準C語言庫只提供了一些通用的邏輯運算方法以及字符串處理,當然字符串在C語言看來也是一種操作內存的方法,所以單純

實現使用Nginx+uWSGI+Django方法部署Django程序

設置 wsgi alias admin 生效 server static 出現 mar 新建一個XML文件: djangochina_socket.xml,將它放在/data/www/org_management目錄下: Nginx采用8077端口與uWSGI通訊,請確保此

不要質疑的付出,這些都會是一種累積一種沈澱,它們會默默鋪路,只為成為更優秀的人

只為 王者歸來 今天 一個 學習 的人 mage com bsp 更新一下今天的學習進度:以後每天都會更新,倘若有啥感悟想說的話也會一起發出來,希望更多的人能和我一起堅持下去:   1.每天背誦50個英文單詞,復習鞏固了60個單詞,進度: 850/3486   2.

11成為一名初級的iOS開發——零基礎

最近發行iPhone8和11月份要出售的iPhone X又掀起了一波iPhone購買熱潮,為什麽蘋果這麽深受大眾的喜愛呢?當然要歸功於強大的iOS系統,不同於Android,iOS並不需要很高的硬件配置就可以發揮很高的效能。這也使得許多開發者們躍躍欲試的想要成為一名iOS的開發者,據美國的一項調查顯示,

ios審核4.3被拒? 別擔心 這幾的 App 順利過審

ios審核 蘋果審核 蘋果審核4.3 ios4.3 最近有許多開發者遇到了因為審核條款 4.3(後文統一簡稱 4.3)審核條款 4.3(後文統一簡稱 4.3),這種情況 常見於大家上傳重復應用的時候,因為App Store 已經有了很多相似的應用 而被打回,今天我們來分享下 4.3 被拒的處理

分鐘看明白到底什麽是Activity --java

Activity 什麽是Activity 寫這篇文章的目的主要是項目組開發第一次使用總結的一點小經驗,不足之處打架多多探討.1.什麽是工作流?以請假為例,現在大多公司的後臺流程是這樣的 a.郵件提出申請 b.上級回郵件同意或其他方式c.上級請假記錄 d.月底將請假上繳公司 e.人事錄電

18種聰明的思維方式,成為高人!

image com 分享圖片 ima src info 分享 思維方式 jpg 18種聰明的思維方式,讓你成為高人!

做好這四徹底成功杜絕亞馬遜跟賣

blog -o ase 堅持 roc 第一步 很多 tin 亞馬遜賣家 熟悉亞馬遜平臺的都知道,自己辛苦經營的Listing被無良商家跟賣真的非常痛苦,眼看著自己精心打造的listing銷量有些起色,這個時候卻被一個吸血蟲盯上了,被跟賣的listing銷量每天下降,轉化率也

買不起蘋果Xs?《海賊王》中的這些惡魔果實分分鐘成為土豪

是的 ofo ref 不想 是你 終究 one 投資 沒有 今天,蘋果手機頒布了它們2018年的新品iphone Xs。 不外這個手機卻由於超高的售價讓許多網民非凡很是的不滿。 雖然像這種土豪手機,小編是想都不敢想的,手內中的諾基亞還能用一段時刻。 除非我能擁有《海賊王》中

一個思維習慣,成為架構師

版本 如同 內容 中產 窗口 測試 ron 支付 工具 程序員的迷茫不僅僅是面對技術繁雜的無力感,更重要的是因為長期埋沒於軟件 世界的浩大的分工體系中,無法看清從業務到軟件架構的價值鏈條,無法清楚定位自 己在分工體系的位置,處理不好自身與技術、業務的關系所致。 很多程序員打

這30個以太坊開發示例,成為80萬都挖不走的區塊鏈人才!

2018年已過了大半,幣圈跌跌蕩蕩,而鏈圈的人在等待鳳凰涅槃,熊市專心做技術,牛市才能一展身手、衝破雲霄! 本文主要告訴你,如何成為一名優秀的以太坊開發者! 如果你是以太坊開發者中的“老司機”,請直接看最後一部分:30個為你量身定做的挑戰示例! 如果你是以太坊