├── CODE_OF_CONDUCT.md ├── GoBlog ├── README.md ├── data.sqlite ├── db.go ├── go.mod ├── go.sum ├── images │ └── 1662832742598308748.jpeg ├── main.go └── templates │ ├── article.html │ ├── base.html │ ├── edit.html │ ├── index.html │ └── new.html ├── LICENSE ├── README.md ├── awesomeProject └── error-handling │ ├── go.mod │ ├── main.go │ ├── network.go │ ├── readfile.go │ ├── readfiles.go │ ├── readfiles_concurrent.go │ └── verifypath.go ├── database-sql-package-goproject ├── README.md ├── connection │ └── connection.go ├── go.mod ├── go.sum ├── insert │ └── insert.go ├── multiple │ └── multiple.go ├── prepared │ └── prepared.go ├── single │ └── single.go ├── table-01.sql ├── table-02.sql └── transaction │ └── transaction.go ├── get-started-with-redis ├── .idea │ ├── .gitignore │ └── runConfigurations │ │ ├── RedisDemo_1__ping.xml │ │ ├── RedisDemo_2__get_set.xml │ │ ├── RedisDemo_3__expire.xml │ │ ├── RedisDemo_4__pipeline.xml │ │ ├── RedisDemo_5__transaction.xml │ │ ├── RedisDemo_6__pub_sub.xml │ │ ├── RedisDemo__pipeline_benchmark.xml │ │ └── RedisDemo__reset_data.xml ├── README.md ├── expiringkeys.go ├── getandset.go ├── go.mod ├── go.sum ├── main.go ├── ping.go ├── pipeline.go ├── pipeline_test.go ├── pubsub.go ├── redisclient.go ├── resetdata.go ├── testdata │ └── setup.redis └── transaction.go ├── go-db-comparison ├── README.md ├── benchmarks │ ├── benchmark.go │ ├── benchmark_test.go │ ├── sqlc_generated │ │ ├── db.go │ │ ├── models.go │ │ ├── query.sql │ │ ├── query.sql.go │ │ ├── schema.sql │ │ └── sqlc.yaml │ └── students.sql.go ├── examples │ ├── database-sql │ │ └── database-sql.go │ ├── gorm │ │ └── gorm.go │ ├── setup.sql │ ├── sqlc │ │ ├── db.go │ │ ├── models.go │ │ ├── query.sql │ │ ├── query.sql.go │ │ ├── schema.sql │ │ ├── sqlc.go │ │ └── sqlc.yaml │ └── sqlx │ │ └── sqlx.go ├── go.mod └── go.sum ├── go-gin-react ├── go-gin-react-part1 │ ├── go.mod │ ├── go.sum │ ├── main.go │ └── schema.sql ├── go-gin-react-part2 │ ├── chat-ui │ │ ├── .gitignore │ │ ├── package-lock.json │ │ ├── package.json │ │ ├── public │ │ │ ├── favicon.ico │ │ │ ├── index.html │ │ │ ├── logo192.png │ │ │ ├── logo512.png │ │ │ ├── manifest.json │ │ │ └── robots.txt │ │ └── src │ │ │ ├── App.js │ │ │ ├── ChannelsList.js │ │ │ ├── CreateUser.js │ │ │ ├── Login.js │ │ │ ├── MainChat.js │ │ │ ├── MessageEntry.js │ │ │ ├── MessagesPanel.js │ │ │ └── index.js │ ├── database.db │ ├── go.mod │ ├── go.sum │ ├── main.go │ ├── package-lock.json │ ├── package.json │ └── schema.sql └── go-gin-react-part3 │ ├── chat-ui │ ├── .gitignore │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── robots.txt │ └── src │ │ ├── App.js │ │ ├── ChannelsList.js │ │ ├── CreateUser.js │ │ ├── Login.js │ │ ├── MainChat.js │ │ ├── MessageEntry.js │ │ ├── MessagesPanel.js │ │ ├── index.js │ │ └── setupProxy.js │ ├── database.db │ ├── go.mod │ ├── go.sum │ ├── main.go │ └── schema.sql ├── go-rest-demo ├── README.md ├── cmd │ ├── gin │ │ ├── main.go │ │ └── main_test.http │ ├── gorilla │ │ ├── main.go │ │ └── main_test.http │ └── standardlib │ │ ├── main.go │ │ ├── main_test.go │ │ └── main_test.http ├── go.mod ├── go.sum ├── pkg │ └── recipes │ │ ├── models.go │ │ ├── recipeMemStore.go │ │ └── recipeMemStore_test.go └── testdata │ ├── ham_and_cheese_recipe.json │ └── ham_and_cheese_with_butter_recipe.json ├── mock-testing ├── README.md └── fetchuser │ ├── .idea │ ├── .gitignore │ ├── fetchuser.iml │ ├── runConfigurations │ │ ├── TestProcessUser_All.xml │ │ ├── TestProcessUser_HigherOrderFunctions.xml │ │ ├── TestProcessUser_HttpTest.xml │ │ ├── TestProcessUser_InterfaceMock1.xml │ │ ├── TestProcessUser_InterfaceMock2.xml │ │ ├── TestProcessUser_Mockgen.xml │ │ └── TestProcessUser_TestifyMock.xml │ └── vcs.xml │ ├── fetchuser.go │ ├── fetchuser_higherorderfunctions.go │ ├── fetchuser_higherorderfunctions_test.go │ ├── fetchuser_httptest_test.go │ ├── fetchuser_interface_test.go │ ├── fetchuser_mockgen_mocks.go │ ├── fetchuser_mockgen_test.go │ ├── fetchuser_testify_test.go │ ├── go.mod │ └── go.sum └── testing-guide ├── README.md ├── fooer.go ├── fooer_test.go ├── go.mod └── go.sum /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | This code of conduct outlines our expectations for all those who participate in our open source projects and communities (community programs), as well as the consequences for unacceptable behaviour. We invite all those who participate to help us create safe and positive experiences for everyone. Communities mirror the societies in which they exist and positive action is essential to counteract the many forms of inequality and abuses of power that exist in society. 4 | 5 | ## How to behave 6 | 7 | The following behaviours are expected and requested of all community members: 8 | 9 | - Participate in an authentic and active way. In doing so, you contribute to the health and longevity of this community. 10 | - Exercise consideration, respect and empathy in your speech and actions. Remember, we have all been through different stages of learning when adopting technologies. 11 | - Refrain from demeaning, discriminatory, or harassing behaviour and speech. 12 | - Disagreements on things are fine, argumentative behaviour or trolling are not. 13 | 14 | ## How not to behave 15 | 16 | - Do not perform threats of violence or use violent language directed against another person. 17 | - Do not make jokes of sexist, racist, homophobic, transphobic, ableist or otherwise discriminatory nature, or use language of this nature. 18 | - Do not post or display sexually explicit or violent material. 19 | - Do not post or threaten to post other people's personally identifying information ("doxing"). 20 | - Do not make personal insults, particularly those related to gender, sexual orientation, race, religion, or disability. 21 | - Do not engage in sexual attention. This includes, sexualised comments or jokes and sexual advances. 22 | - Do not advocate for, or encourage, any of the above behaviour. 23 | 24 | Please take into account that online communities bring together people from many different cultures and backgrounds. It's important to understand that sometimes the combination of cultural differences and online interaction can lead to misunderstandings. That is why having empathy is very important. 25 | 26 | ## How to report issues 27 | 28 | If someone is acting inappropriately or violating this Code of Conduct in any shape or form, and they are not receptive to your feedback or you prefer not to confront them, please reach out to JetBrains via 29 | 30 | ## Consequences of Unacceptable Behaviour 31 | 32 | Unacceptable behaviour from any community member will not be tolerated. Anyone asked to stop unacceptable behaviour is expected to comply immediately. If a community member engages in unacceptable behaviour, JetBrains and/or community organisers may take any action they deem appropriate, up to and including a temporary ban or permanent expulsion from the community without warning. 33 | 34 | ## License and attribution 35 | 36 | The license is based off of The Citizen Code of Conduct is distributed by Stumptown Syndicate under a Creative Commons Attribution-ShareAlike license. 37 | -------------------------------------------------------------------------------- /GoBlog/README.md: -------------------------------------------------------------------------------- 1 | # Blog with Go Templates 2 | 3 | This is a blog with templates. It uses the `html/template` package to generate web pages. 4 | It uses the Chi router to set up the routes and an SQLite database to store the articles. 5 | 6 | Find the tutorial [here](https://blog.jetbrains.com/go/2022/11/08/build-a-blog-with-go-templates/). 7 | -------------------------------------------------------------------------------- /GoBlog/data.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains/go-code-samples/f03be7aaaf80c54b27a10173456974ba05a18fbf/GoBlog/data.sqlite -------------------------------------------------------------------------------- /GoBlog/db.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "database/sql" 4 | 5 | func connect() (*sql.DB, error) { 6 | var err error 7 | db, err = sql.Open("sqlite3", "./data.sqlite") 8 | if err != nil { 9 | return nil, err 10 | } 11 | 12 | sqlStmt := ` 13 | create table if not exists articles (id integer not null primary key autoincrement, title text, content text); 14 | ` 15 | 16 | _, err = db.Exec(sqlStmt) 17 | if err != nil { 18 | return nil, err 19 | } 20 | 21 | return db, nil 22 | } 23 | 24 | func dbCreateArticle(article *Article) error { 25 | query, err := db.Prepare("insert into articles(title,content) values (?,?)") 26 | defer query.Close() 27 | 28 | if err != nil { 29 | return err 30 | } 31 | _, err = query.Exec(article.Title, article.Content) 32 | 33 | if err != nil { 34 | return err 35 | } 36 | 37 | return nil 38 | } 39 | 40 | func dbGetAllArticles() ([]*Article, error) { 41 | query, err := db.Prepare("select id, title, content from articles") 42 | defer query.Close() 43 | 44 | if err != nil { 45 | return nil, err 46 | } 47 | result, err := query.Query() 48 | 49 | if err != nil { 50 | return nil, err 51 | } 52 | articles := make([]*Article, 0) 53 | for result.Next() { 54 | data := new(Article) 55 | err := result.Scan( 56 | &data.ID, 57 | &data.Title, 58 | &data.Content, 59 | ) 60 | if err != nil { 61 | return nil, err 62 | } 63 | articles = append(articles, data) 64 | } 65 | 66 | return articles, nil 67 | } 68 | 69 | func dbGetArticle(articleID string) (*Article, error) { 70 | query, err := db.Prepare("select id, title, content from articles where id = ?") 71 | defer query.Close() 72 | 73 | if err != nil { 74 | return nil, err 75 | } 76 | result := query.QueryRow(articleID) 77 | data := new(Article) 78 | err = result.Scan(&data.ID, &data.Title, &data.Content) 79 | 80 | if err != nil { 81 | return nil, err 82 | } 83 | 84 | return data, nil 85 | } 86 | 87 | func dbUpdateArticle(id string, article *Article) error { 88 | query, err := db.Prepare("update articles set (title, content) = (?,?) where id=?") 89 | defer query.Close() 90 | 91 | if err != nil { 92 | return err 93 | } 94 | _, err = query.Exec(article.Title, article.Content, id) 95 | 96 | if err != nil { 97 | return err 98 | } 99 | 100 | return nil 101 | } 102 | 103 | func dbDeleteArticle(id string) error { 104 | query, err := db.Prepare("delete from articles where id=?") 105 | defer query.Close() 106 | 107 | if err != nil { 108 | return err 109 | } 110 | _, err = query.Exec(id) 111 | 112 | if err != nil { 113 | return err 114 | } 115 | 116 | return nil 117 | } 118 | -------------------------------------------------------------------------------- /GoBlog/go.mod: -------------------------------------------------------------------------------- 1 | module GoBlog 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/go-chi/chi/v5 v5.0.7 7 | github.com/mattn/go-sqlite3 v1.14.15 8 | ) 9 | -------------------------------------------------------------------------------- /GoBlog/go.sum: -------------------------------------------------------------------------------- 1 | github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8= 2 | github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= 3 | github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI= 4 | github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= 5 | -------------------------------------------------------------------------------- /GoBlog/images/1662832742598308748.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains/go-code-samples/f03be7aaaf80c54b27a10173456974ba05a18fbf/GoBlog/images/1662832742598308748.jpeg -------------------------------------------------------------------------------- /GoBlog/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "encoding/json" 7 | "fmt" 8 | "github.com/go-chi/chi/v5" 9 | "github.com/go-chi/chi/v5/middleware" 10 | _ "github.com/mattn/go-sqlite3" 11 | "html/template" 12 | "io" 13 | "net/http" 14 | "os" 15 | "path/filepath" 16 | "strconv" 17 | "time" 18 | ) 19 | 20 | var router *chi.Mux 21 | var db *sql.DB 22 | 23 | type Article struct { 24 | ID int `json:"id"` 25 | Title string `json:"title"` 26 | Content template.HTML `json:"content"` 27 | } 28 | 29 | func catch(err error) { 30 | if err != nil { 31 | fmt.Println(err) 32 | panic(err) 33 | } 34 | } 35 | 36 | func init() { 37 | router = chi.NewRouter() 38 | router.Use(middleware.Recoverer) 39 | 40 | var err error 41 | db, err = connect() 42 | catch(err) 43 | } 44 | 45 | func main() { 46 | router = chi.NewRouter() 47 | router.Use(middleware.Recoverer) 48 | 49 | var err error 50 | db, err = connect() 51 | catch(err) 52 | 53 | router.Use(ChangeMethod) 54 | router.Get("/", GetAllArticles) 55 | router.Post("/upload", UploadHandler) // Add this 56 | router.Get("/images/*", ServeImages) // Add this 57 | router.Route("/articles", func(r chi.Router) { 58 | r.Get("/", NewArticle) 59 | r.Post("/", CreateArticle) 60 | r.Route("/{articleID}", func(r chi.Router) { 61 | r.Use(ArticleCtx) 62 | r.Get("/", GetArticle) // GET /articles/1234 63 | r.Put("/", UpdateArticle) // PUT /articles/1234 64 | r.Delete("/", DeleteArticle) // DELETE /articles/1234 65 | r.Get("/edit", EditArticle) // GET /articles/1234/edit 66 | }) 67 | }) 68 | 69 | err = http.ListenAndServe(":8005", router) 70 | catch(err) 71 | } 72 | 73 | func UploadHandler(w http.ResponseWriter, r *http.Request) { 74 | const MAX_UPLOAD_SIZE = 10 << 20 75 | r.Body = http.MaxBytesReader(w, r.Body, MAX_UPLOAD_SIZE) 76 | if err := r.ParseMultipartForm(MAX_UPLOAD_SIZE); err != nil { 77 | http.Error(w, "The uploaded file is too big. Please choose an file that's less than 10MB in size", http.StatusBadRequest) 78 | return 79 | } 80 | 81 | file, fileHeader, err := r.FormFile("file") 82 | if err != nil { 83 | http.Error(w, err.Error(), http.StatusBadRequest) 84 | return 85 | } 86 | 87 | defer file.Close() 88 | 89 | // Create the uploads folder if it doesn't already exist 90 | err = os.MkdirAll("./images", os.ModePerm) 91 | if err != nil { 92 | http.Error(w, err.Error(), http.StatusInternalServerError) 93 | return 94 | } 95 | 96 | // Create a new file in the uploads directory 97 | filename := fmt.Sprintf("/images/%d%s", time.Now().UnixNano(), filepath.Ext(fileHeader.Filename)) 98 | dst, err := os.Create("." + filename) 99 | if err != nil { 100 | fmt.Println(err) 101 | http.Error(w, err.Error(), http.StatusInternalServerError) 102 | return 103 | } 104 | 105 | defer dst.Close() 106 | 107 | // Copy the uploaded file to the specified destination 108 | _, err = io.Copy(dst, file) 109 | if err != nil { 110 | http.Error(w, err.Error(), http.StatusInternalServerError) 111 | return 112 | } 113 | fmt.Println(filename) 114 | response, _ := json.Marshal(map[string]string{"location": filename}) 115 | w.Header().Set("Content-Type", "application/json") 116 | w.WriteHeader(http.StatusCreated) 117 | w.Write(response) 118 | } 119 | 120 | func ServeImages(w http.ResponseWriter, r *http.Request) { 121 | fmt.Println(r.URL) 122 | fs := http.StripPrefix("/images/", http.FileServer(http.Dir("./images"))) 123 | fs.ServeHTTP(w, r) 124 | } 125 | 126 | func ChangeMethod(next http.Handler) http.Handler { 127 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 128 | if r.Method == http.MethodPost { 129 | switch method := r.PostFormValue("_method"); method { 130 | case http.MethodPut: 131 | fallthrough 132 | case http.MethodPatch: 133 | fallthrough 134 | case http.MethodDelete: 135 | r.Method = method 136 | default: 137 | } 138 | } 139 | next.ServeHTTP(w, r) 140 | }) 141 | } 142 | 143 | func ArticleCtx(next http.Handler) http.Handler { 144 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 145 | articleID := chi.URLParam(r, "articleID") 146 | article, err := dbGetArticle(articleID) 147 | if err != nil { 148 | fmt.Println(err) 149 | http.Error(w, http.StatusText(404), 404) 150 | return 151 | } 152 | ctx := context.WithValue(r.Context(), "article", article) 153 | next.ServeHTTP(w, r.WithContext(ctx)) 154 | }) 155 | } 156 | 157 | func GetAllArticles(w http.ResponseWriter, r *http.Request) { 158 | articles, err := dbGetAllArticles() 159 | catch(err) 160 | 161 | t, _ := template.ParseFiles("templates/base.html", "templates/index.html") 162 | err = t.Execute(w, articles) 163 | catch(err) 164 | } 165 | 166 | func NewArticle(w http.ResponseWriter, r *http.Request) { 167 | t, _ := template.ParseFiles("templates/base.html", "templates/new.html") 168 | err := t.Execute(w, nil) 169 | catch(err) 170 | } 171 | 172 | func CreateArticle(w http.ResponseWriter, r *http.Request) { 173 | title := r.FormValue("title") 174 | content := r.FormValue("content") 175 | article := &Article{ 176 | Title: title, 177 | Content: template.HTML(content), 178 | } 179 | 180 | err := dbCreateArticle(article) 181 | catch(err) 182 | http.Redirect(w, r, "/", http.StatusFound) 183 | } 184 | 185 | func GetArticle(w http.ResponseWriter, r *http.Request) { 186 | article := r.Context().Value("article").(*Article) 187 | t, _ := template.ParseFiles("templates/base.html", "templates/article.html") 188 | err := t.Execute(w, article) 189 | catch(err) 190 | } 191 | 192 | func EditArticle(w http.ResponseWriter, r *http.Request) { 193 | article := r.Context().Value("article").(*Article) 194 | 195 | t, _ := template.ParseFiles("templates/base.html", "templates/edit.html") 196 | err := t.Execute(w, article) 197 | catch(err) 198 | } 199 | 200 | func UpdateArticle(w http.ResponseWriter, r *http.Request) { 201 | article := r.Context().Value("article").(*Article) 202 | 203 | title := r.FormValue("title") 204 | content := r.FormValue("content") 205 | newArticle := &Article{ 206 | Title: title, 207 | Content: template.HTML(content), 208 | } 209 | fmt.Println(newArticle.Content) 210 | err := dbUpdateArticle(strconv.Itoa(article.ID), newArticle) 211 | catch(err) 212 | http.Redirect(w, r, fmt.Sprintf("/articles/%d", article.ID), http.StatusFound) 213 | } 214 | 215 | func DeleteArticle(w http.ResponseWriter, r *http.Request) { 216 | article := r.Context().Value("article").(*Article) 217 | err := dbDeleteArticle(strconv.Itoa(article.ID)) 218 | catch(err) 219 | 220 | http.Redirect(w, r, "/", http.StatusFound) 221 | } 222 | -------------------------------------------------------------------------------- /GoBlog/templates/article.html: -------------------------------------------------------------------------------- 1 | {{define "title"}}{{.Title}}{{end}} 2 | {{define "scripts"}}{{end}} 3 | {{define "body"}} 4 |

{{.Title}}

5 |
6 | {{.Content}} 7 |
8 |
9 | Edit 10 |
11 | 12 | 13 |
14 |
15 | {{end}} -------------------------------------------------------------------------------- /GoBlog/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ template "title" . }} 6 | {{ template "scripts" }} 7 | 8 | 9 | {{ template "body" . }} 10 | 11 | -------------------------------------------------------------------------------- /GoBlog/templates/edit.html: -------------------------------------------------------------------------------- 1 | {{define "title"}}Create new article{{end}} 2 | {{define "scripts"}} 3 | 4 | 18 | {{end}} 19 | {{define "body"}} 20 |
21 | 22 | 23 | 24 | 25 |
26 | {{end}} -------------------------------------------------------------------------------- /GoBlog/templates/index.html: -------------------------------------------------------------------------------- 1 | {{define "title"}}All articles{{end}} 2 | {{define "scripts"}}{{end}} 3 | {{define "body"}} 4 | {{if eq (len .) 0}} 5 | Nothing to see here 6 | {{end}} 7 | {{range .}} 8 |
9 | {{.Title}} 10 |
11 | {{end}} 12 |

13 | Create new article 14 | 15 |

16 | {{end}} -------------------------------------------------------------------------------- /GoBlog/templates/new.html: -------------------------------------------------------------------------------- 1 | {{define "title"}}Create new article{{end}} 2 | {{define "scripts"}} 3 | 4 | 18 | {{end}} 19 | {{define "body"}} 20 |
21 | 22 | 23 | 24 |
25 | {{end}} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![JetBrains team project](https://jb.gg/badges/team.svg)](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub) 2 | 3 | # go-code-samples 4 | 5 | This repository contains code samples for tutorials published on [the GoLand blog](https://blog.jetbrains.com/go/category/tutorials/). Each folder has a separate README with a link to the corresponding article. 6 | -------------------------------------------------------------------------------- /awesomeProject/error-handling/go.mod: -------------------------------------------------------------------------------- 1 | module error-handling 2 | 3 | go 1.20 4 | -------------------------------------------------------------------------------- /awesomeProject/error-handling/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "io/fs" 6 | "log" 7 | ) 8 | 9 | func main() { 10 | log.SetFlags(0) // no time stamp 11 | 12 | // Read a single file 13 | log.SetPrefix("Reading a single file: ") 14 | 15 | _, err := ReadFile("no/file") 16 | if err != nil { 17 | log.Println("err = ", err) 18 | } 19 | 20 | // Unwrap the error returned by os.Open() 21 | log.Println("errors.Unwrap(err) = ", errors.Unwrap(err)) 22 | 23 | // Confirm that the error is, or wraps, an fs.ErrNotExist error 24 | log.Println("err is fs.ErrNotExist:", errors.Is(err, fs.ErrNotExist)) 25 | 26 | // Confirm that the error is, or wraps, an fs.PathError. 27 | // errors.As() assigns the unwrapped PathError to target. 28 | // This allows reading PathError's Path field. 29 | target := &fs.PathError{} 30 | if errors.As(err, &target) { 31 | log.Printf("err as PathError: path is '%s'\n", target.Path) 32 | log.Printf("err as PathError: op is '%s'\n", target.Op) 33 | } 34 | 35 | // Read files concurrently - handling context errors 36 | log.SetPrefix("Reading files concurrently: ") 37 | 38 | _, err = ReadFilesConcurrently([]string{"no/file/a", "no/file/b", "no/file/c"}) 39 | log.Println("err = ", err) 40 | 41 | // Read multiple files 42 | log.SetPrefix("Reading multiple files: ") 43 | 44 | // Passing an empty slice triggers a plain error 45 | _, err = ReadFiles([]string{}) 46 | log.Println("err = ", err) 47 | 48 | // Passing multiple paths of non-existing files triggers a joined error 49 | _, err = ReadFiles([]string{"no/file/a", "no/file/b", "no/file/c"}) 50 | log.Println("joined errors = ", err) 51 | 52 | // Unwrap the errors inside the joined error 53 | // A joined error does not have the method "func Unwrap() error" 54 | // because it does not wrap a single error but rather a slice of errors. 55 | // Therefore, errors.Unwrap() cannot unwrap the errors and returns nil. 56 | log.Println("errors.Unwrap(err) = ", errors.Unwrap(err)) 57 | 58 | // To unwrap a joined error, you can type-assert that err has 59 | // an Unwrap() method that returns a slice of errors. 60 | e, ok := err.(interface{ Unwrap() []error }) 61 | if ok { 62 | log.Println("e.Unwrap() = ", e.Unwrap()) 63 | } 64 | 65 | // Network errors 66 | log.SetPrefix("Network errors: ") 67 | 68 | err = connectToTCPServer() 69 | log.Println("err = ", err) 70 | 71 | // Recover from a panic 72 | log.SetPrefix("Recovering from a panic: ") 73 | 74 | // This example is at the end of main, because the panic 75 | // causes main to exit. Only the deferred function is 76 | // called before exiting. 77 | 78 | defer func() { 79 | // Is this func invoked from a panic? 80 | if r := recover(); r != nil { 81 | // Yes: recover from the panic 82 | log.Printf("isValidPath panicked: error is '%v'\n", r) 83 | // ... 84 | } 85 | }() 86 | 87 | // isValidPath panics because of an invalid regexp. 88 | if isValidPath("/path/to/file") { 89 | _, _ = ReadFile("/path/to/file") 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /awesomeProject/error-handling/network.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "net" 8 | ) 9 | 10 | // Connect to a TCP server and check the error. use errors.As() to unwrap the net.OpError, and test if the error is transient. 11 | func connectToTCPServer() error { 12 | var err error 13 | var conn net.Conn 14 | for retry := 3; retry > 0; retry-- { 15 | conn, err = net.Dial("tcp", "127.0.0.1:12345") 16 | if err != nil { 17 | // Check if err is a net.OpError 18 | opErr := &net.OpError{} 19 | if errors.As(err, &opErr) { 20 | log.Println("err is net.OpError:", opErr.Error()) 21 | // test if the error is temporary 22 | if opErr.Temporary() { 23 | log.Printf("Retrying...\n") 24 | continue 25 | } 26 | retry = 0 27 | } 28 | } 29 | } 30 | if err != nil { 31 | return fmt.Errorf("connect failed: %w", err) 32 | } 33 | defer conn.Close() 34 | // send or receive data 35 | return nil 36 | } 37 | -------------------------------------------------------------------------------- /awesomeProject/error-handling/readfile.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "os" 8 | ) 9 | 10 | func ReadFile(path string) ([]byte, error) { 11 | if path == "" { 12 | // Create an error with errors.New() 13 | return nil, errors.New("path is empty") 14 | } 15 | f, err := os.Open(path) 16 | if err != nil { 17 | // Wrap the error. 18 | // If the format string uses %w to format the error, 19 | // fmt.Errorf() returns an error that has the 20 | // method "func Unwrap() error" implemented. 21 | return nil, fmt.Errorf("open failed: %w", err) 22 | } 23 | defer f.Close() 24 | 25 | buf, err := io.ReadAll(f) 26 | if err != nil { 27 | return nil, fmt.Errorf("read failed: %w", err) 28 | } 29 | return buf, nil 30 | } 31 | -------------------------------------------------------------------------------- /awesomeProject/error-handling/readfiles.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | ) 7 | 8 | func ReadFiles(paths []string) ([][]byte, error) { 9 | var errs error 10 | var contents [][]byte 11 | 12 | if len(paths) == 0 { 13 | // Create a new error with fmt.Errorf() (but without using %w): 14 | return nil, fmt.Errorf("no paths provided: paths slice is %v", paths) 15 | } 16 | 17 | for _, path := range paths { 18 | content, err := ReadFile(path) 19 | if err != nil { 20 | // Join all errors that occur into errs. 21 | // The returned error type implements method "func Unwrap() []error". 22 | // (Note that the return type is a slice.) 23 | errs = errors.Join(errs, fmt.Errorf("reading %s failed: %w", path, err)) 24 | continue 25 | } 26 | contents = append(contents, content) 27 | } 28 | 29 | // Some files may have been read, some may have failed to be read. 30 | // Therefore, ReadFiles returns both return values, regardless 31 | // of whether there have been errors. 32 | return contents, errs 33 | } 34 | -------------------------------------------------------------------------------- /awesomeProject/error-handling/readfiles_concurrent.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "math/rand" 8 | "sync" 9 | "time" 10 | ) 11 | 12 | // Goal: Read multiple files concurrently. All files must be read successfully. 13 | // If one goroutine fails, cancel all the other goroutines. 14 | // 15 | // The cancelled goroutines can inspect the error from canceling the context with ctx.Err().. 16 | 17 | func ReadFilesConcurrently(paths []string) ([][]byte, error) { 18 | var contents [][]byte 19 | 20 | ctx, cancel := context.WithCancelCause(context.Background()) 21 | defer cancel(nil) 22 | 23 | resCh := make(chan []byte) 24 | wg := sync.WaitGroup{} 25 | 26 | for _, path := range paths { 27 | wg.Add(1) 28 | go func(ctx context.Context, cancel context.CancelCauseFunc, p string, resCh chan<- []byte, wg *sync.WaitGroup) { 29 | time.Sleep(time.Duration(rand.Intn(10)) * time.Microsecond) // simulate workload 30 | select { 31 | case <-ctx.Done(): 32 | log.Printf("ReadFilesConcurrently (goroutine): Context canceled for path %s: %v", p, ctx.Err()) 33 | wg.Done() 34 | return 35 | default: 36 | // If an error occurs here, cancel all the other goroutines 37 | content, err := ReadFile(p) 38 | if err != nil { 39 | cancel(fmt.Errorf("error reading %s: %w", p, err)) 40 | log.Printf("ReadFilesConcurrently (goroutine): Context canceled for path %s: %v", p, err) 41 | wg.Done() 42 | return 43 | } 44 | resCh <- content 45 | } 46 | }(ctx, cancel, path, resCh, &wg) 47 | } 48 | 49 | go func() { 50 | wg.Wait() 51 | close(resCh) 52 | }() 53 | 54 | for c := range resCh { 55 | contents = append(contents, c) 56 | } 57 | 58 | if e := ctx.Err(); e != nil { 59 | return nil, fmt.Errorf("ReadFilesConcurrently: %w", e) 60 | } 61 | return contents, nil 62 | } 63 | -------------------------------------------------------------------------------- /awesomeProject/error-handling/verifypath.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "regexp" 4 | 5 | func isValidPath(p string) bool { 6 | pathRe := regexp.MustCompile(`(invalid regular expression`) 7 | return pathRe.MatchString(p) 8 | } 9 | -------------------------------------------------------------------------------- /database-sql-package-goproject/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with The database/sql Package 2 | 3 | This hands-on tutorial will show you how to get started with the database/sql package. 4 | 5 | Find the tutorial [here](https://blog.jetbrains.com/go/2023/02/28/getting-started-with-the-database-sql-package/). -------------------------------------------------------------------------------- /database-sql-package-goproject/connection/connection.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "log" 7 | "os" 8 | 9 | "github.com/go-sql-driver/mysql" 10 | ) 11 | 12 | var db *sql.DB 13 | 14 | type Album struct { 15 | ID int64 16 | Title string 17 | Artist string 18 | Price float32 19 | Quantity int64 20 | } 21 | 22 | func main() { 23 | // Capture connection properties. 24 | cfg := mysql.Config{ 25 | User: os.Getenv("DBUSER"), 26 | Passwd: os.Getenv("DBPASS"), 27 | Net: "tcp", 28 | Addr: "127.0.0.1:3306", 29 | DBName: "recordings", 30 | } 31 | // Get a database handle. 32 | var err error 33 | db, err = sql.Open("mysql", cfg.FormatDSN()) 34 | if err != nil { 35 | log.Fatal(err) 36 | } 37 | 38 | pingErr := db.Ping() 39 | if pingErr != nil { 40 | log.Fatal(pingErr) 41 | } 42 | fmt.Println("Connected!") 43 | } 44 | -------------------------------------------------------------------------------- /database-sql-package-goproject/go.mod: -------------------------------------------------------------------------------- 1 | module database-sql-package-goproject 2 | 3 | go 1.19 4 | 5 | require github.com/go-sql-driver/mysql v1.7.0 // indirect 6 | -------------------------------------------------------------------------------- /database-sql-package-goproject/go.sum: -------------------------------------------------------------------------------- 1 | github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= 2 | github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= 3 | -------------------------------------------------------------------------------- /database-sql-package-goproject/insert/insert.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "log" 7 | "os" 8 | 9 | "github.com/go-sql-driver/mysql" 10 | ) 11 | 12 | var db *sql.DB 13 | 14 | type Album struct { 15 | ID int64 16 | Title string 17 | Artist string 18 | Price float32 19 | Quantity int64 20 | } 21 | 22 | func main() { 23 | // Capture connection properties. 24 | cfg := mysql.Config{ 25 | User: os.Getenv("DBUSER"), 26 | Passwd: os.Getenv("DBPASS"), 27 | Net: "tcp", 28 | Addr: "127.0.0.1:3306", 29 | DBName: "recordings", 30 | } 31 | // Get a database handle. 32 | var err error 33 | db, err = sql.Open("mysql", cfg.FormatDSN()) 34 | if err != nil { 35 | log.Fatal(err) 36 | } 37 | 38 | pingErr := db.Ping() 39 | if pingErr != nil { 40 | log.Fatal(pingErr) 41 | } 42 | fmt.Println("Connected!") 43 | 44 | albID, err := addAlbum(Album{ 45 | Title: "The Modern Sound of Betty Carter", 46 | Artist: "Betty Carter", 47 | Price: 49.99, 48 | Quantity: 10, 49 | }) 50 | if err != nil { 51 | log.Fatal(err) 52 | } 53 | fmt.Printf("ID of added album: %v\n", albID) 54 | } 55 | 56 | // addAlbum adds the specified album to the database, 57 | // returning the album ID of the new entry 58 | func addAlbum(alb Album) (int64, error) { 59 | result, err := db.Exec("INSERT INTO album (title, artist, price, quantity) VALUES (?, ?, ?, ?)", alb.Title, alb.Artist, alb.Price, alb.Quantity) 60 | if err != nil { 61 | return 0, fmt.Errorf("addAlbum: %v", err) 62 | } 63 | id, err := result.LastInsertId() 64 | if err != nil { 65 | return 0, fmt.Errorf("addAlbum: %v", err) 66 | } 67 | return id, nil 68 | } 69 | -------------------------------------------------------------------------------- /database-sql-package-goproject/multiple/multiple.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "log" 7 | "os" 8 | 9 | "github.com/go-sql-driver/mysql" 10 | ) 11 | 12 | var db *sql.DB 13 | 14 | type Album struct { 15 | ID int64 16 | Title string 17 | Artist string 18 | Price float32 19 | Quantity int64 20 | } 21 | 22 | func main() { 23 | // Capture connection properties. 24 | cfg := mysql.Config{ 25 | User: os.Getenv("DBUSER"), 26 | Passwd: os.Getenv("DBPASS"), 27 | Net: "tcp", 28 | Addr: "127.0.0.1:3306", 29 | DBName: "recordings", 30 | } 31 | // Get a database handle. 32 | var err error 33 | db, err = sql.Open("mysql", cfg.FormatDSN()) 34 | if err != nil { 35 | log.Fatal(err) 36 | } 37 | 38 | pingErr := db.Ping() 39 | if pingErr != nil { 40 | log.Fatal(pingErr) 41 | } 42 | fmt.Println("Connected!") 43 | 44 | albums, err := albumsByArtist("John Coltrane") 45 | if err != nil { 46 | log.Fatal(err) 47 | } 48 | fmt.Printf("Albums found: %v\n", albums) 49 | } 50 | 51 | // albumsByArtist queries for albums that have the specified artist name. 52 | func albumsByArtist(name string) ([]Album, error) { 53 | // An albums slice to hold data from returned rows. 54 | var albums []Album 55 | 56 | rows, err := db.Query("SELECT * FROM album WHERE artist = ?", name) 57 | if err != nil { 58 | return nil, fmt.Errorf("albumsByArtist %q: %v", name, err) 59 | } 60 | defer rows.Close() 61 | // Loop through rows, using Scan to assign column data to struct fields. 62 | for rows.Next() { 63 | var alb Album 64 | if err := rows.Scan(&alb.ID, &alb.Title, &alb.Artist, &alb.Price, &alb.Quantity); err != nil { 65 | return nil, fmt.Errorf("albumsByArtist %q: %v", name, err) 66 | } 67 | albums = append(albums, alb) 68 | } 69 | if err := rows.Err(); err != nil { 70 | return nil, fmt.Errorf("albumsByArtist %q: %v", name, err) 71 | } 72 | return albums, nil 73 | } 74 | -------------------------------------------------------------------------------- /database-sql-package-goproject/prepared/prepared.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "log" 7 | "os" 8 | 9 | "github.com/go-sql-driver/mysql" 10 | ) 11 | 12 | var db *sql.DB 13 | 14 | type Album struct { 15 | ID int64 16 | Title string 17 | Artist string 18 | Price float32 19 | Quantity int64 20 | } 21 | 22 | func main() { 23 | // Capture connection properties. 24 | cfg := mysql.Config{ 25 | User: os.Getenv("DBUSER"), 26 | Passwd: os.Getenv("DBPASS"), 27 | Net: "tcp", 28 | Addr: "127.0.0.1:3306", 29 | DBName: "recordings", 30 | } 31 | // Get a database handle. 32 | var err error 33 | db, err = sql.Open("mysql", cfg.FormatDSN()) 34 | if err != nil { 35 | log.Fatal(err) 36 | } 37 | 38 | pingErr := db.Ping() 39 | if pingErr != nil { 40 | log.Fatal(pingErr) 41 | } 42 | fmt.Println("Connected!") 43 | 44 | // Hard-code ID 2 here to test the query. 45 | Album, err := albumByID(2) 46 | if err != nil { 47 | log.Fatal(err) 48 | } 49 | fmt.Printf("Album found: %v\n", Album) 50 | } 51 | 52 | // AlbumByID retrieves the specified album. 53 | func albumByID(id int) (Album, error) { 54 | // Define a prepared statement. You'd typically define the statement 55 | // elsewhere and save it for use in functions such as this one. 56 | stmt, err := db.Prepare("SELECT * FROM album WHERE id = ?") 57 | if err != nil { 58 | log.Fatal(err) 59 | } 60 | 61 | var album Album 62 | 63 | // Execute the prepared statement, passing in an id value for the 64 | // parameter whose placeholder is ? 65 | err = stmt.QueryRow(id).Scan(&album.ID, &album.Title, &album.Artist, &album.Price, &album.Quantity) 66 | if err != nil { 67 | if err == sql.ErrNoRows { 68 | // Handle the case of no rows returned. 69 | } 70 | return album, err 71 | } 72 | return album, nil 73 | } 74 | -------------------------------------------------------------------------------- /database-sql-package-goproject/single/single.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "log" 7 | "os" 8 | 9 | "github.com/go-sql-driver/mysql" 10 | ) 11 | 12 | var db *sql.DB 13 | 14 | type Album struct { 15 | ID int64 16 | Title string 17 | Artist string 18 | Price float32 19 | Quantity int64 20 | } 21 | 22 | func main() { 23 | // Capture connection properties. 24 | cfg := mysql.Config{ 25 | User: os.Getenv("DBUSER"), 26 | Passwd: os.Getenv("DBPASS"), 27 | Net: "tcp", 28 | Addr: "127.0.0.1:3306", 29 | DBName: "recordings", 30 | } 31 | // Get a database handle. 32 | var err error 33 | db, err = sql.Open("mysql", cfg.FormatDSN()) 34 | if err != nil { 35 | log.Fatal(err) 36 | } 37 | 38 | pingErr := db.Ping() 39 | if pingErr != nil { 40 | log.Fatal(pingErr) 41 | } 42 | fmt.Println("Connected!") 43 | 44 | // Hard-code ID 2 here to test the query. 45 | alb, err := albumByID(2) 46 | if err != nil { 47 | log.Fatal(err) 48 | } 49 | fmt.Printf("Album found: %v\n", alb) 50 | } 51 | 52 | // albumByID queries for the album with the specified ID. 53 | func albumByID(id int64) (Album, error) { 54 | // An album to hold data from the returned row. 55 | var alb Album 56 | 57 | row := db.QueryRow("SELECT * FROM album WHERE id = ?", id) 58 | if err := row.Scan(&alb.ID, &alb.Title, &alb.Artist, &alb.Price, &alb.Quantity); err != nil { 59 | if err == sql.ErrNoRows { 60 | return alb, fmt.Errorf("albumsById %d: no such album", id) 61 | } 62 | return alb, fmt.Errorf("albumsById %d: %v", id, err) 63 | } 64 | return alb, nil 65 | } 66 | -------------------------------------------------------------------------------- /database-sql-package-goproject/table-01.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS album; 2 | CREATE TABLE album ( 3 | id INT AUTO_INCREMENT NOT NULL, 4 | title VARCHAR(128) NOT NULL, 5 | artist VARCHAR(255) NOT NULL, 6 | price DECIMAL(5,2) NOT NULL, 7 | quantity INT UNSIGNED, 8 | PRIMARY KEY (`id`) 9 | ); 10 | 11 | INSERT INTO album 12 | (title, artist, price, quantity) 13 | VALUES 14 | ('Blue Train', 'John Coltrane', 56.99, 5), 15 | ('Giant Steps', 'John Coltrane', 63.99, 62), 16 | ('Jeru', 'Gerry Mulligan', 17.99, 0), 17 | ('Sarah Vaughan', 'Sarah Vaughan', 34.98, 127); 18 | -------------------------------------------------------------------------------- /database-sql-package-goproject/table-02.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS album_trx; 2 | CREATE TABLE album_trx ( 3 | trx_id INT AUTO_INCREMENT NOT NULL, 4 | trx_check INT UNSIGNED, 5 | PRIMARY KEY (`trx_id`) 6 | ); 7 | -------------------------------------------------------------------------------- /database-sql-package-goproject/transaction/transaction.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "fmt" 7 | "log" 8 | "os" 9 | 10 | "github.com/go-sql-driver/mysql" 11 | ) 12 | 13 | var db *sql.DB 14 | 15 | type Album struct { 16 | ID int64 17 | Title string 18 | Artist string 19 | Price float32 20 | Quantity int64 21 | } 22 | 23 | type Album_trx struct { 24 | TRX_ID int64 25 | TRX_CHECK int64 26 | } 27 | 28 | func main() { 29 | // Capture connection properties. 30 | cfg := mysql.Config{ 31 | User: os.Getenv("DBUSER"), 32 | Passwd: os.Getenv("DBPASS"), 33 | Net: "tcp", 34 | Addr: "127.0.0.1:3306", 35 | DBName: "recordings", 36 | } 37 | // Get a database handle. 38 | var err error 39 | db, err = sql.Open("mysql", cfg.FormatDSN()) 40 | if err != nil { 41 | log.Fatal(err) 42 | } 43 | 44 | pingErr := db.Ping() 45 | if pingErr != nil { 46 | log.Fatal(pingErr) 47 | } 48 | fmt.Println("Connected!") 49 | 50 | // Start the transaction 51 | ctx := context.Background() 52 | tx, err := db.BeginTx(ctx, nil) 53 | if err != nil { 54 | log.Fatal(err) 55 | } 56 | // First query 57 | _, err = tx.ExecContext(ctx, "INSERT INTO album (title, artist, price, quantity) VALUES ('Master of Puppets', 'Metallica', '49', '1')") 58 | if err != nil { 59 | tx.Rollback() 60 | return 61 | } 62 | 63 | // Second query 64 | _, err = tx.ExecContext(ctx, "INSERT INTO album_trx (trx_check) VALUES (1)") 65 | if err != nil { 66 | tx.Rollback() 67 | fmt.Println("Transaction declined") 68 | return 69 | } 70 | 71 | // If no errors, commit the transaction 72 | err = tx.Commit() 73 | if err != nil { 74 | log.Fatal(err) 75 | } 76 | fmt.Println("Transaction accepted!") 77 | } 78 | -------------------------------------------------------------------------------- /get-started-with-redis/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /get-started-with-redis/.idea/runConfigurations/RedisDemo_1__ping.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /get-started-with-redis/.idea/runConfigurations/RedisDemo_2__get_set.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /get-started-with-redis/.idea/runConfigurations/RedisDemo_3__expire.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /get-started-with-redis/.idea/runConfigurations/RedisDemo_4__pipeline.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /get-started-with-redis/.idea/runConfigurations/RedisDemo_5__transaction.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /get-started-with-redis/.idea/runConfigurations/RedisDemo_6__pub_sub.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /get-started-with-redis/.idea/runConfigurations/RedisDemo__pipeline_benchmark.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /get-started-with-redis/.idea/runConfigurations/RedisDemo__reset_data.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /get-started-with-redis/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Redis in Go 2 | 3 | This tutorial demonstrates how to use Redis, the popular in-memory database that doubles as cache, pub/sub service, and streaming service, with Go and GoLand. 4 | 5 | Find the tutorial [here](). -------------------------------------------------------------------------------- /get-started-with-redis/expiringkeys.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/redis/go-redis/v9" 7 | "time" 8 | ) 9 | 10 | func expiringKeys(client *redis.Client) error { 11 | ctx := context.Background() 12 | 13 | // Add a temporary player 14 | err := client.HSet(ctx, "player:10", "name", "Crymyios", "score", 0, "team", "Knucklewimp", "challenges_completed", 0).Err() 15 | if err != nil { 16 | return fmt.Errorf("cannot set player:10: %w", err) 17 | } 18 | 19 | // Set an expiration time for player:10 20 | if !client.Expire(ctx, "player:10", time.Second).Val() { 21 | return fmt.Errorf("cannot set expiration time for player:10") 22 | } 23 | 24 | // Get player:10 25 | for i := 0; i < 3; i++ { 26 | val, err := client.HGet(ctx, "player:10", "name").Result() 27 | if err != nil { 28 | fmt.Printf("player:10 has expired: %v\n", err) 29 | return nil 30 | } 31 | fmt.Printf("player:10's name: %s\n", val) 32 | time.Sleep(500 * time.Millisecond) 33 | } 34 | return nil 35 | } 36 | -------------------------------------------------------------------------------- /get-started-with-redis/getandset.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/redis/go-redis/v9" 7 | "golang.org/x/text/cases" 8 | "golang.org/x/text/language" 9 | ) 10 | 11 | // Task: The quest is a lowercase string. Change it to title case. 12 | // Then print the challenges in the order they have to be completed. 13 | 14 | func getAndSet(client *redis.Client) error { 15 | ctx := context.Background() 16 | 17 | quest, err := client.Get(ctx, "quest").Result() 18 | if err != nil { 19 | return fmt.Errorf("cannot get quest: %w", err) 20 | } 21 | 22 | quest = cases.Title(language.English).String(quest) 23 | 24 | err = client.Set(ctx, "quest", quest, 0).Err() 25 | if err != nil { 26 | return fmt.Errorf("cannot update quest: %w", err) 27 | } 28 | 29 | fmt.Printf("Quest is now: %s\n", client.Get(ctx, "quest").Val()) 30 | 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /get-started-with-redis/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/JetBrains/go-code-samples/get-started-with-redis 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/redis/go-redis/v9 v9.0.5 7 | golang.org/x/text v0.9.0 8 | ) 9 | 10 | require ( 11 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 12 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 13 | ) 14 | -------------------------------------------------------------------------------- /get-started-with-redis/go.sum: -------------------------------------------------------------------------------- 1 | github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao= 2 | github.com/bsm/ginkgo/v2 v2.7.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w= 3 | github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y= 4 | github.com/bsm/gomega v1.26.0/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= 5 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 6 | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 7 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 8 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 9 | github.com/redis/go-redis/v9 v9.0.5 h1:CuQcn5HIEeK7BgElubPP8CGtE0KakrnbBSTLjathl5o= 10 | github.com/redis/go-redis/v9 v9.0.5/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk= 11 | golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= 12 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 13 | golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= 14 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 15 | golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= 16 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 17 | golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= 18 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 19 | -------------------------------------------------------------------------------- /get-started-with-redis/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | ) 8 | 9 | const ( 10 | dbconn = "localhost:6379" 11 | db = 0 12 | ) 13 | 14 | func usage() { 15 | fmt.Println(`Usage: go run main.go 16 | Where is one of: 17 | - ping: run the ping command 18 | - getset: get and set a string value 19 | - expire: set an expiring key 20 | - pipeline: run a batch of commands 21 | - transaction: run a batch of commands that must all succeed 22 | - pubsub: send a message to a channel and listen to the channel 23 | - reset: restore the initial data set`) 24 | } 25 | 26 | func run() error { 27 | // Create a new client from a connection string and a database number (0-15) 28 | client := newClient(dbconn, 0) 29 | 30 | if len(os.Args) < 2 { 31 | usage() 32 | return nil 33 | } 34 | 35 | switch os.Args[1] { 36 | case "ping": 37 | // Ping the redis server and fetch some database information. 38 | fmt.Printf("\nPing: Test the connection\n") 39 | ping(client) 40 | 41 | case "getset": 42 | // Get and set a string value. 43 | fmt.Printf("\nGet/Set: Update the quest to title case\n") 44 | err := getAndSet(client) 45 | if err != nil { 46 | return fmt.Errorf("getAndSet failed: %w", err) 47 | } 48 | 49 | case "expire": 50 | // Set an expiring key and wait for it to expire 51 | fmt.Printf("\nExpire: Add a player temporarily\n") 52 | err := expiringKeys(client) 53 | if err != nil { 54 | return fmt.Errorf("expiringKeys failed: %w", err) 55 | } 56 | 57 | case "pipeline": 58 | // Run a batch of commands. 59 | fmt.Printf("\nPipeline: Update score and challenges_completed for team Snarkdumbthimble\n") 60 | err := pipeline(client) 61 | if err != nil { 62 | return fmt.Errorf("pipeline failed: %w", err) 63 | } 64 | 65 | case "transaction": 66 | // Run several commands that must all succeed. 67 | // If any of them fails, the transaction will be canceled. 68 | fmt.Printf("\nTransaction: Rearrange the teams\n") 69 | err := transaction(client) 70 | if err != nil { 71 | return fmt.Errorf("transaction failed: %w", err) 72 | } 73 | 74 | case "pubsub": 75 | // Send messages to a publish/receive channel. 76 | // Listen to the channel and receive the messages. 77 | fmt.Printf("\nPub/Sub: Publish challenges to subscribed teams\n") 78 | err := pubsub(client) 79 | if err != nil { 80 | return fmt.Errorf("pubsub stopped: %w", err) 81 | } 82 | 83 | case "reset": 84 | // Reset the database 85 | fmt.Printf("\nReset the database\n") 86 | err := resetdata(client) 87 | if err != nil { 88 | return fmt.Errorf("cannot reset database: %w", err) 89 | } 90 | default: 91 | usage() 92 | } 93 | return nil 94 | } 95 | 96 | func main() { 97 | err := run() 98 | if err != nil { 99 | log.Println(err) 100 | os.Exit(1) 101 | } 102 | os.Exit(0) 103 | } 104 | -------------------------------------------------------------------------------- /get-started-with-redis/ping.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/redis/go-redis/v9" 7 | ) 8 | 9 | func ping(client *redis.Client) error { 10 | // For the demo, we need only a background context 11 | ctx := context.Background() 12 | // Ping the redis server. It should respond with "PONG". 13 | fmt.Println(client.Ping(ctx)) 14 | 15 | // Get the client info. 16 | info, err := client.ClientInfo(ctx).Result() 17 | if err != nil { 18 | return fmt.Errorf("method ClientInfo failed: %w", err) 19 | } 20 | 21 | fmt.Printf("%#v\n", info) 22 | return nil 23 | } 24 | -------------------------------------------------------------------------------- /get-started-with-redis/pipeline.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/redis/go-redis/v9" 7 | ) 8 | 9 | // Task: Update the score and the challenges_completed 10 | // for team Snarkdumbthimble that has finished challenge #1. 11 | 12 | func pipeline(client *redis.Client) error { 13 | ctx := context.Background() 14 | 15 | _, err := client.Pipelined(ctx, func(pipe redis.Pipeliner) error { 16 | err := pipe.HSet(ctx, "player:7", "score", 15, "challenges_completed", 1).Err() 17 | if err != nil { 18 | return err 19 | } 20 | err = pipe.HSet(ctx, "player:8", "score", 18, "challenges_completed", 1).Err() 21 | if err != nil { 22 | return err 23 | } 24 | err = pipe.HSet(ctx, "player:9", "score", 12, "challenges_completed", 1).Err() 25 | 26 | return err 27 | }) 28 | if err != nil { 29 | return fmt.Errorf("pipelined failed: %w", err) 30 | } 31 | 32 | fmt.Printf("Player 7's score: %s, challenges completed: %s\n", 33 | client.HGet(ctx, "player:7", "score").Val(), 34 | client.HGet(ctx, "player:7", "challenges_completed").Val()) 35 | fmt.Printf("Player 8's score: %s, challenges completed: %s\n", 36 | client.HGet(ctx, "player:8", "score").Val(), 37 | client.HGet(ctx, "player:8", "challenges_completed").Val()) 38 | fmt.Printf("Player 9's score: %s, challenges completed: %s\n", 39 | client.HGet(ctx, "player:9", "score").Val(), 40 | client.HGet(ctx, "player:9", "challenges_completed").Val()) 41 | 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /get-started-with-redis/pipeline_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/redis/go-redis/v9" 7 | "testing" 8 | ) 9 | 10 | func incrementScorePipe(client *redis.Client, player string) error { 11 | ctx := context.Background() 12 | client.Pipelined(ctx, func(pipe redis.Pipeliner) error { 13 | for i := 0; i < 1000; i++ { 14 | err := pipe.HIncrBy(ctx, player, "score", 1).Err() 15 | if err != nil { 16 | return fmt.Errorf("cannot increment score for player %s to %d: %w", player, i, err) 17 | } 18 | } 19 | pipe.HSet(ctx, player, "score", 1) 20 | return nil 21 | }) 22 | return nil 23 | } 24 | 25 | func incrementScoreNoPipe(client *redis.Client, player string) error { 26 | ctx := context.Background() 27 | for i := 0; i < 1000; i++ { 28 | err := client.HIncrBy(ctx, player, "score", 1).Err() 29 | if err != nil { 30 | return fmt.Errorf("cannot increment score for player %s to %d: %w", player, i, err) 31 | } 32 | } 33 | client.HSet(ctx, player, "score", 1) 34 | return nil 35 | } 36 | 37 | func BenchmarkPipeline(b *testing.B) { 38 | client := newClient(dbconn, 0) 39 | b.Run("PipelinedHIncrBy", func(b *testing.B) { 40 | for i := 0; i < b.N; i++ { 41 | incrementScorePipe(client, "player:1") 42 | } 43 | }) 44 | } 45 | 46 | func BenchmarkNoPipeline(b *testing.B) { 47 | client := newClient(dbconn, 0) 48 | b.Run("HIncrBy", func(b *testing.B) { 49 | for i := 0; i < b.N; i++ { 50 | incrementScoreNoPipe(client, "player:2") 51 | } 52 | }) 53 | } 54 | -------------------------------------------------------------------------------- /get-started-with-redis/pubsub.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/redis/go-redis/v9" 7 | "time" 8 | ) 9 | 10 | // Task: Send each challenge to the "challenge" channel. 11 | // Each team is subscribed to the "challenge" channel and enters the challenge upon receiving. 12 | 13 | const ( 14 | // the name of the PubSub channel 15 | pubsubChan = "challenge" 16 | ) 17 | 18 | // Res is the result of reading the pubsub channel 19 | type Res struct { 20 | result string 21 | err error 22 | } 23 | 24 | // Team manages a team's subscription 25 | // Each team uses its own client to subscribe to the "challenge" channel 26 | type Team struct { 27 | name string 28 | client *redis.Client 29 | channel *redis.PubSub 30 | } 31 | 32 | // getTeams scans the database for "team:*" keys 33 | // and returns a slice of Team structs with names filled from the keys 34 | func getTeams(client *redis.Client) []Team { 35 | ctx := context.Background() 36 | teams := make([]Team, 3) 37 | teamsets := make([]string, 0, 3) 38 | keys := make([]string, 0, 3) 39 | var cursor uint64 40 | for { 41 | // Scan returns a slice of matches. The count may or may not be reached 42 | // in the first call to Scan, so the code needs to call Scan in a loop and 43 | // append the found keys to the teamsets slice until the cursor "returns to 0". 44 | var err error 45 | keys, cursor, err = client.Scan(ctx, cursor, "team:*", 3).Result() 46 | if err != nil { 47 | break 48 | } 49 | teamsets = append(teamsets, keys...) 50 | if cursor == 0 { 51 | break 52 | } 53 | } 54 | // Lazily assume that the scan has returned 3 team sets 55 | for i := 0; i < 3; i++ { 56 | teams[i].name = teamsets[i] 57 | // each team uses its own client 58 | teams[i].client = newClient(dbconn, 0) 59 | } 60 | return teams 61 | } 62 | 63 | // subscribe subscribes to the "challenge" channel 64 | // and waits for the subscription to be completed 65 | func (team *Team) subscribe() error { 66 | ctx := context.Background() 67 | // Subscribe to the "challenge" channel 68 | pubSub := team.client.Subscribe(ctx, pubsubChan) 69 | 70 | // The first Subscribe() call creates the channel. 71 | // Until that point, any attempt to publish something fails. 72 | reply, err := pubSub.Receive(ctx) 73 | if err != nil { 74 | return fmt.Errorf("subscribing to channel '%s' failed: %w", pubsubChan, err) 75 | } 76 | // Expected response type is "*Subscription". Otherwise, something failed. 77 | switch reply.(type) { 78 | case *redis.Subscription: 79 | // Success! 80 | case *redis.Message: 81 | // The channel is already active and contains messages, hence also a success 82 | case *redis.Pong: 83 | // letL's call it a success 84 | default: 85 | return fmt.Errorf("subscribing to a channel failed: received a reply of type %T, expected: *redis.Subscription", reply) 86 | } 87 | 88 | team.channel = pubSub 89 | 90 | fmt.Printf("%s subscribed to channel '%s'\n", team.name, pubsubChan) 91 | return nil 92 | } 93 | 94 | // receive receives messages from the "challenge" channel. 95 | // It starts a goroutine that reads from the pubsub channel until 96 | // the channel is closed or the context is done. 97 | func (team *Team) receive(ctx context.Context, resChan chan<- Res) { 98 | ch := team.channel.Channel() 99 | defer close(resChan) 100 | for { 101 | select { 102 | case msg, ok := <-ch: 103 | if !ok { 104 | // The pubsub channel has been closed 105 | return 106 | } 107 | resChan <- Res{fmt.Sprintf("%s received challenge '%s'", team.name, msg.Payload), nil} 108 | case <-ctx.Done(): 109 | resChan <- Res{"", ctx.Err()} 110 | return 111 | } 112 | } 113 | } 114 | 115 | // publish publishes the challenge to the "challenge" channel 116 | func publish(client *redis.Client, challenge string) error { 117 | ctx := context.Background() 118 | fmt.Printf("publishing challenge '%s'\n", challenge) 119 | return client.Publish(ctx, pubsubChan, challenge).Err() 120 | } 121 | 122 | // pubsub subscribes to the "challenge" channel, publishes the challenges, 123 | // and receives the published messages. 124 | func pubsub(client *redis.Client) (err error) { 125 | ctx := context.Background() 126 | 127 | // Step 1: subscribe each team 128 | teams := getTeams(client) 129 | for i := 0; i < 3; i++ { 130 | err = teams[i].subscribe() 131 | if err != nil { 132 | return fmt.Errorf("subscribing failed: %w", err) 133 | } 134 | } 135 | 136 | // Step 2: publish challenges 137 | // Read the challenges from the sorted set "challenges" and publish them 138 | for i := int64(0); i < 5; i++ { 139 | challenge := client.ZRange(ctx, "challenges", i, i).Val()[0] 140 | err = publish(client, challenge) 141 | if err != nil { 142 | return fmt.Errorf("cannot publish challenge %s: %w", challenge, err) 143 | } 144 | } 145 | // Close the channel after one second, to terminate the receive loops. 146 | time.AfterFunc(time.Second, func() { 147 | teams[0].channel.Close() 148 | fmt.Println(`PubSub channel "challenges" closed`) 149 | }) 150 | 151 | // Step 3: receive published messages 152 | rch := make(chan Res) 153 | for i := 0; i < 3; i++ { 154 | go teams[i].receive(ctx, rch) 155 | } 156 | for msg := range rch { 157 | if msg.err != nil { 158 | return fmt.Errorf("cannot receive challenge: %w", msg.err) 159 | } 160 | fmt.Println(msg.result) 161 | } 162 | 163 | return nil 164 | } 165 | -------------------------------------------------------------------------------- /get-started-with-redis/redisclient.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/redis/go-redis/v9" 4 | 5 | func newClient(conn string, db int) *redis.Client { 6 | return redis.NewClient(&redis.Options{ 7 | Addr: conn, 8 | DB: db, 9 | Password: "", 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /get-started-with-redis/resetdata.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/csv" 6 | "fmt" 7 | "github.com/redis/go-redis/v9" 8 | "io" 9 | "os" 10 | ) 11 | 12 | const ( 13 | setupfile = "testdata/setup.redis" 14 | ) 15 | 16 | // resetdata flushes all data from the current database (db 0) 17 | // and runs the commands from file setup.redis to set up the database. 18 | func resetdata(client *redis.Client) error { 19 | ctx := context.Background() 20 | 21 | // read file "setup.redis" line by line 22 | setup, err := os.Open(setupfile) 23 | if err != nil { 24 | return fmt.Errorf("cannot open %s: %w", setupfile, err) 25 | } 26 | defer setup.Close() 27 | 28 | client.FlushDB(ctx) // FlushDB never fails. 29 | 30 | csv := csv.NewReader(setup) 31 | csv.Comma = ' ' 32 | csv.FieldsPerRecord = -1 // Variable number of fields per line 33 | 34 | for { 35 | cmd, err := csv.Read() 36 | if err == io.EOF { 37 | return nil 38 | } 39 | if err != nil { 40 | return fmt.Errorf("csv: cannot read a line from %s: %w", setupfile, err) 41 | } 42 | 43 | // cmd is a slice of strings, Do() expects a slice of 'any'. 44 | // The memory layout of the two slice types is not the same, 45 | // so we need to convert cmd to a slice of 'any'. 46 | doCmd := make([]interface{}, len(cmd)) 47 | for i, v := range cmd { 48 | doCmd[i] = v 49 | } 50 | 51 | err = client.Do(ctx, doCmd...).Err() 52 | if err != nil { 53 | return fmt.Errorf("resetdata: cannot execute '%v': %w", cmd, err) 54 | } 55 | } 56 | return nil 57 | } 58 | -------------------------------------------------------------------------------- /get-started-with-redis/testdata/setup.redis: -------------------------------------------------------------------------------- 1 | FLUSHDB 2 | SET quest "the chicken coop catastrophe" 3 | SET description "Players must rescue a group of chickens that have been taken hostage by a group of evil goblins." 4 | HSET "player:1" name Sykios score 0 team Dorkfoot challenges_completed 0 5 | HSET "player:2" name Nidios score 0 team Dorkfoot challenges_completed 0 6 | HSET "player:3" name Tiaitia score 0 team Dorkfoot challenges_completed 0 7 | HSET "player:4" name Belaeos score 0 team Knucklewimp challenges_completed 0 8 | HSET "player:5" name Polytia score 0 team Knucklewimp challenges_completed 0 9 | HSET "player:6" name Moritia score 0 team Knucklewimp challenges_completed 0 10 | HSET "player:7" name Daryos score 0 team Snarkdumbthimble challenges_completed 0 11 | HSET "player:8" name Blalios score 0 team Snarkdumbthimble challenges_completed 0 12 | HSET "player:9" name Ighteatia score 0 team Snarkdumbthimble challenges_completed 0 13 | SADD "team:Dorkfoot" Sykios Nidios Tiaitia 14 | SADD "team:Knucklewimp" Belaeos Polytia Moritia 15 | SADD "team:Snarkdumbthimble" Daryos Blalios Ighteatia 16 | ZADD "challenges" 1 "Enter the hidden dungeon" 2 "Find the chicken coop" 3 "Defeat the goblins" 4 "Rescue the chickens" 5 "Escape the dungeon" 17 | -------------------------------------------------------------------------------- /get-started-with-redis/transaction.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/redis/go-redis/v9" 7 | ) 8 | 9 | // Task: 10 | // - Create a new team: Grumblebum 11 | // - Move Sykios, Nidios, and Belaeos to the new team 12 | // - Move Tiaitia to team Knucklewimp 13 | // - Remove team Dorkfoot 14 | 15 | func transaction(client *redis.Client) error { 16 | ctx := context.Background() 17 | 18 | _, err := client.TxPipelined(ctx, func(pipe redis.Pipeliner) error { 19 | // Move Sykios to team Grumblebum 20 | err := pipe.HSet(ctx, "player:1", "team", "Grumblebum").Err() 21 | if err != nil { 22 | return err 23 | } 24 | // Move Nidios to team Grumblebum 25 | err = pipe.HSet(ctx, "player:2", "team", "Grumblebum").Err() 26 | if err != nil { 27 | return err 28 | } 29 | // Move Belaeos to team Grumblebum 30 | err = pipe.HSet(ctx, "player:4", "team", "Grumblebum").Err() 31 | if err != nil { 32 | return err 33 | } 34 | // Move Tiaitia to team Knucklewimp 35 | err = pipe.HSet(ctx, "player:3", "team", "Knucklewimp").Err() 36 | if err != nil { 37 | return err 38 | } 39 | 40 | // Team update: remove Belaeos from team Knucklewimp 41 | err = pipe.SRem(ctx, "team:Knucklewimp", "Belaeos").Err() 42 | if err != nil { 43 | return err 44 | } 45 | 46 | // Team update: add Tiaitia to team Knucklewimp 47 | err = pipe.SAdd(ctx, "team:Knucklewimp", "Tiaitia").Err() 48 | if err != nil { 49 | return err 50 | } 51 | 52 | // Add team Grumblebum 53 | err = pipe.SAdd(ctx, "team:Grumblebum", "Sykios", "Nidios", "Belaeos").Err() 54 | if err != nil { 55 | return err 56 | } 57 | 58 | // Remove team Dorkfoot. A set is removed by removing all elements. 59 | err = pipe.SRem(ctx, "team:Dorkfoot", "Sykios", "Nidios", "Tiaitia").Err() 60 | if err != nil { 61 | return err 62 | } 63 | 64 | return nil 65 | }) 66 | if err != nil { 67 | return fmt.Errorf("TxPipelined failed: %w", err) 68 | } 69 | 70 | fmt.Printf("Sykios's new team: %s\n", client.HGet(ctx, "player:1", "team").Val()) 71 | fmt.Printf("Belaeos's new team: %s\n", client.HGet(ctx, "player:4", "team").Val()) 72 | fmt.Printf("Tiaitia's new team: %s\n", client.HGet(ctx, "player:3", "team").Val()) 73 | fmt.Printf("Team Grumblebum: %s\n", client.SMembers(ctx, "team:Grumblebum").Val()) 74 | fmt.Printf("Team Knucklewimp: %s\n", client.SMembers(ctx, "team:Knucklewimp").Val()) 75 | return nil 76 | } 77 | -------------------------------------------------------------------------------- /go-db-comparison/README.md: -------------------------------------------------------------------------------- 1 | # Comparing database/sql, GORM, sqlx, and sqlc 2 | 3 | This article compares the database/sql package with 3 other Go packages, namely: sqlx, sqlc, and GORM. The comparison focuses on 3 areas – features, ease of use, and performance. 4 | 5 | Find the article [here](https://blog.jetbrains.com/go/2023/04/27/comparing-db-packages/). -------------------------------------------------------------------------------- /go-db-comparison/benchmarks/benchmark.go: -------------------------------------------------------------------------------- 1 | package benchmarks 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "log" 7 | "time" 8 | 9 | "gorm.io/driver/mysql" 10 | "gorm.io/gorm" 11 | 12 | _ "github.com/go-sql-driver/mysql" 13 | "github.com/jmoiron/sqlx" 14 | sqlc "github.com/rexfordnyrk/go-db-comparison/benchmarks/sqlc_generated" 15 | ) 16 | 17 | func init() { 18 | var err error 19 | // Opening a database connection. 20 | db, err = sql.Open("mysql", "theuser:thepass@tcp(localhost:3306)/thedb?multiStatements=true&parseTime=true") 21 | if err != nil { 22 | panic(err) 23 | } 24 | 25 | //sqlx connection using existing db connection 26 | dbx = sqlx.NewDb(db, "mysql") 27 | 28 | //sqlc connection using existing db connection 29 | dbc = sqlc.New(db) 30 | 31 | //gorm connection using existing db connection 32 | gdb, err = gorm.Open(mysql.New(mysql.Config{Conn: db})) 33 | if err != nil { 34 | panic(err) 35 | } 36 | } 37 | 38 | var ( 39 | gdb *gorm.DB 40 | db *sql.DB 41 | dbx *sqlx.DB 42 | dbc *sqlc.Queries 43 | ) 44 | 45 | func setup() { 46 | clear() 47 | table := `CREATE TABLE students ( 48 | id bigint NOT NULL AUTO_INCREMENT, 49 | fname varchar(50) not null, 50 | lname varchar(50) not null, 51 | date_of_birth datetime not null, 52 | email varchar(50) not null, 53 | address varchar(50) not null, 54 | gender varchar(50) not null, 55 | PRIMARY KEY (id) 56 | );` 57 | _, err := db.Exec(table) 58 | if err != nil { 59 | panic(err) 60 | } 61 | 62 | //inserting records 63 | _, err = db.Exec(records) 64 | if err != nil { 65 | panic(err) 66 | } 67 | } 68 | 69 | func clear() { 70 | _, err := db.Exec(`DROP TABLE IF EXISTS students;`) 71 | if err != nil { 72 | panic(err) 73 | } 74 | } 75 | 76 | type Student struct { 77 | ID int64 78 | Fname string 79 | Lname string 80 | DateOfBirth time.Time `db:"date_of_birth"` 81 | Email string 82 | Address string 83 | Gender string 84 | } 85 | 86 | func DbSqlQueryStudentWithLimit(limit int) { 87 | var students []Student 88 | rows, err := db.Query("SELECT * FROM students limit ?", limit) 89 | if err != nil { 90 | log.Fatalf("DbSqlQueryStudentWithLimit %d %v", limit, err) 91 | } 92 | defer rows.Close() 93 | 94 | // Loop through rows, using Scan to assign column data to struct fields. 95 | for rows.Next() { 96 | var s Student 97 | if err := rows.Scan(&s.ID, &s.Fname, &s.Lname, &s.DateOfBirth, &s.Email, &s.Address, &s.Gender); err != nil { 98 | log.Fatalf("DbSqlQueryStudentWithLimit %d %v", limit, err) 99 | } 100 | students = append(students, s) 101 | } 102 | if err := rows.Err(); err != nil { 103 | log.Fatalf("DbSqlQueryStudentWithLimit %d %v", limit, err) 104 | } 105 | } 106 | 107 | func SqlxQueryStudentWithLimit(limit int) { 108 | var students []Student 109 | err := dbx.Select(&students, "SELECT * FROM students LIMIT ?", limit) 110 | if err != nil { 111 | log.Fatalf("SqlxQueryStudentWithLimit %d %v", limit, err) 112 | } 113 | } 114 | 115 | func SqlcQueryStudentWithLimit(limit int) { 116 | _, err := dbc.FetchStudents(context.Background(), int32(limit)) 117 | if err != nil { 118 | log.Fatalf("SqlcQueryStudentWithLimit %d %v", limit, err) 119 | } 120 | } 121 | 122 | func GormQueryStudentWithLimit(limit int) { 123 | var students []Student 124 | if err := gdb.Limit(limit).Find(&students).Error; err != nil { 125 | log.Fatalf("GormQueryStudentWithLimit %d %v", limit, err) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /go-db-comparison/benchmarks/benchmark_test.go: -------------------------------------------------------------------------------- 1 | package benchmarks 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func Benchmark(b *testing.B) { 9 | setup() 10 | defer clear() 11 | 12 | // Benchmark goes in here 13 | limits := []int{ 14 | 1, 15 | 10, 16 | 100, 17 | 1000, 18 | 10000, 19 | 15000, 20 | } 21 | 22 | for _, lim := range limits { // Fetch varying number of rows 23 | 24 | fmt.Printf("================================== BENCHMARKING %d RECORDS ======================================\n", lim) 25 | // Benchmark Database/sql 26 | b.Run(fmt.Sprintf("Database/sql limit:%d ", lim), func(b *testing.B) { 27 | for i := 0; i < b.N; i++ { 28 | DbSqlQueryStudentWithLimit(lim) 29 | } 30 | }) 31 | 32 | // Benchmark Sqlx 33 | b.Run(fmt.Sprintf("Sqlx limit:%d ", lim), func(b *testing.B) { 34 | for i := 0; i < b.N; i++ { 35 | SqlxQueryStudentWithLimit(lim) 36 | } 37 | }) 38 | 39 | // Benchmark Sqlc 40 | b.Run(fmt.Sprintf("Sqlc limit:%d ", lim), func(b *testing.B) { 41 | for i := 0; i < b.N; i++ { 42 | SqlcQueryStudentWithLimit(lim) 43 | } 44 | }) 45 | 46 | // Benchmark GORM 47 | b.Run(fmt.Sprintf("GORM limit:%d ", lim), func(b *testing.B) { 48 | for i := 0; i < b.N; i++ { 49 | GormQueryStudentWithLimit(lim) 50 | } 51 | }) 52 | 53 | fmt.Println("=================================================================================================") 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /go-db-comparison/benchmarks/sqlc_generated/db.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.16.0 4 | 5 | package sqlc 6 | 7 | import ( 8 | "context" 9 | "database/sql" 10 | ) 11 | 12 | type DBTX interface { 13 | ExecContext(context.Context, string, ...interface{}) (sql.Result, error) 14 | PrepareContext(context.Context, string) (*sql.Stmt, error) 15 | QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) 16 | QueryRowContext(context.Context, string, ...interface{}) *sql.Row 17 | } 18 | 19 | func New(db DBTX) *Queries { 20 | return &Queries{db: db} 21 | } 22 | 23 | type Queries struct { 24 | db DBTX 25 | } 26 | 27 | func (q *Queries) WithTx(tx *sql.Tx) *Queries { 28 | return &Queries{ 29 | db: tx, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /go-db-comparison/benchmarks/sqlc_generated/models.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.16.0 4 | 5 | package sqlc 6 | 7 | import ( 8 | "time" 9 | ) 10 | 11 | type Student struct { 12 | ID int64 13 | Fname string 14 | Lname string 15 | DateOfBirth time.Time 16 | Email string 17 | Address string 18 | Gender string 19 | } 20 | -------------------------------------------------------------------------------- /go-db-comparison/benchmarks/sqlc_generated/query.sql: -------------------------------------------------------------------------------- 1 | -- name: FetchStudents :many 2 | SELECT * FROM students LIMIT ?; -------------------------------------------------------------------------------- /go-db-comparison/benchmarks/sqlc_generated/query.sql.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.16.0 4 | // source: query.sql 5 | 6 | package sqlc 7 | 8 | import ( 9 | "context" 10 | ) 11 | 12 | const fetchStudents = `-- name: FetchStudents :many 13 | SELECT id, fname, lname, date_of_birth, email, address, gender FROM students LIMIT ? 14 | ` 15 | 16 | func (q *Queries) FetchStudents(ctx context.Context, limit int32) ([]Student, error) { 17 | rows, err := q.db.QueryContext(ctx, fetchStudents, limit) 18 | if err != nil { 19 | return nil, err 20 | } 21 | defer rows.Close() 22 | var items []Student 23 | for rows.Next() { 24 | var i Student 25 | if err := rows.Scan( 26 | &i.ID, 27 | &i.Fname, 28 | &i.Lname, 29 | &i.DateOfBirth, 30 | &i.Email, 31 | &i.Address, 32 | &i.Gender, 33 | ); err != nil { 34 | return nil, err 35 | } 36 | items = append(items, i) 37 | } 38 | if err := rows.Close(); err != nil { 39 | return nil, err 40 | } 41 | if err := rows.Err(); err != nil { 42 | return nil, err 43 | } 44 | return items, nil 45 | } 46 | -------------------------------------------------------------------------------- /go-db-comparison/benchmarks/sqlc_generated/schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `students` ( 2 | `id` bigint NOT NULL AUTO_INCREMENT, 3 | `fname` varchar(50) not null, 4 | `lname` varchar(50) not null, 5 | `date_of_birth` datetime not null, 6 | `email` varchar(50) not null, 7 | `address` varchar(50) not null, 8 | `gender` varchar(50) not null, 9 | PRIMARY KEY (`id`) 10 | ); -------------------------------------------------------------------------------- /go-db-comparison/benchmarks/sqlc_generated/sqlc.yaml: -------------------------------------------------------------------------------- 1 | version: 1 2 | packages: 3 | - path: "./" 4 | name: "sqlc" 5 | engine: "mysql" 6 | schema: "schema.sql" 7 | queries: "query.sql" -------------------------------------------------------------------------------- /go-db-comparison/examples/database-sql/database-sql.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "log" 7 | "time" 8 | 9 | _ "github.com/go-sql-driver/mysql" 10 | ) 11 | 12 | type Student struct { 13 | ID int 14 | Fname string 15 | Lname string 16 | DateOfBirth time.Time 17 | Email string 18 | Address string 19 | Gender string 20 | } 21 | 22 | var db *sql.DB 23 | 24 | func main() { 25 | var err error 26 | db, err = dbSqlConnect() 27 | if err != nil { 28 | log.Fatal(err) 29 | } 30 | 31 | s := Student{ 32 | Fname: "Leon", 33 | Lname: "Ashling", 34 | DateOfBirth: time.Date(1994, time.August, 14, 23, 51, 42, 0, time.UTC), 35 | Email: "lashling5@senate.gov", 36 | Address: "39 Kipling Pass", 37 | Gender: "Male", 38 | } 39 | 40 | //adding student record to table 41 | sID, err := addStudent(s) 42 | if err != nil { 43 | fmt.Println(err) 44 | } 45 | fmt.Printf("addSudent id: %v \n", sID) 46 | 47 | //selecting student by ID 48 | st, err := studentByID(sID) 49 | if err != nil { 50 | fmt.Println(err) 51 | } 52 | fmt.Printf("studentByID id: %v \n", st) 53 | 54 | students, err := fetchStudents() 55 | if err != nil { 56 | fmt.Println(err) 57 | } 58 | 59 | fmt.Printf("fetchStudents count: %v \n", len(students)) 60 | } 61 | 62 | func dbSqlConnect() (*sql.DB, error) { 63 | // Opening a database connection. 64 | db, err := sql.Open("mysql", "theuser:thepass@tcp(localhost:3306)/thedb?parseTime=true") 65 | if err != nil { 66 | return nil, err 67 | } 68 | fmt.Println("Connected!") 69 | return db, nil 70 | } 71 | 72 | func addStudent(s Student) (int64, error) { 73 | query := "insert into students (fname, lname, date_of_birth, email, gender, address) values (?, ?, ?, ?, ?, ?);" 74 | result, err := db.Exec(query, s.Fname, s.Lname, s.DateOfBirth, s.Email, s.Gender, s.Address) 75 | if err != nil { 76 | return 0, fmt.Errorf("addStudent Error: %v", err) 77 | } 78 | 79 | id, err := result.LastInsertId() 80 | if err != nil { 81 | return 0, fmt.Errorf("addSudent Error: %v", err) 82 | } 83 | 84 | return id, nil 85 | } 86 | 87 | func fetchStudents() ([]Student, error) { 88 | // A slice of Students to hold data from returned rows. 89 | var students []Student 90 | 91 | rows, err := db.Query("SELECT * FROM students") 92 | if err != nil { 93 | return nil, fmt.Errorf("fetchStudents %v", err) 94 | } 95 | defer rows.Close() 96 | 97 | // Loop through rows, using Scan to assign column data to struct fields. 98 | for rows.Next() { 99 | var s Student 100 | if err := rows.Scan(&s.ID, &s.Fname, &s.Lname, &s.DateOfBirth, &s.Email, &s.Address, &s.Gender); err != nil { 101 | return nil, fmt.Errorf("fetchStudents %v", err) 102 | } 103 | students = append(students, s) 104 | } 105 | if err := rows.Err(); err != nil { 106 | return nil, fmt.Errorf("fetchStudents %v", err) 107 | } 108 | 109 | return students, nil 110 | } 111 | 112 | func studentByID(id int64) (Student, error) { 113 | var st Student 114 | 115 | row := db.QueryRow("SELECT * FROM students WHERE id = ?", id) 116 | if err := row.Scan(&st.ID, &st.Fname, &st.Lname, &st.DateOfBirth, &st.Email, &st.Address, &st.Gender); err != nil { 117 | if err == sql.ErrNoRows { 118 | return st, fmt.Errorf("studentById %d: no such student", id) 119 | } 120 | return st, fmt.Errorf("studentById %d: %v", id, err) 121 | } 122 | return st, nil 123 | } 124 | -------------------------------------------------------------------------------- /go-db-comparison/examples/gorm/gorm.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "time" 7 | 8 | "gorm.io/driver/mysql" 9 | "gorm.io/gorm" 10 | ) 11 | 12 | type Student struct { 13 | ID int 14 | Fname string 15 | Lname string 16 | DateOfBirth time.Time 17 | Email string 18 | Address string 19 | Gender string 20 | } 21 | 22 | func main() { 23 | // refer https://github.com/go-sql-driver/mysql#dsn-data-source-name for details 24 | db, err := gorm.Open(mysql.Open("theuser:thepass@tcp(127.0.0.1:3306)/thedb?charset=utf8mb4&parseTime=True&loc=Local")) 25 | if err != nil { 26 | log.Fatal(err) 27 | } 28 | 29 | fmt.Println("Connected!") 30 | 31 | //initializing record to be inserted 32 | s := Student{ 33 | Fname: "Leon", 34 | Lname: "Ashling", 35 | DateOfBirth: time.Date(1994, time.August, 14, 23, 51, 42, 0, time.UTC), 36 | Email: "lashling5@senate.gov", 37 | Address: "39 Kipling Pass", 38 | Gender: "Male", 39 | } 40 | //adds student record and returns the ID into the ID field 41 | db.Create(&s) 42 | fmt.Printf("addSudent id: %v \n", s.ID) 43 | 44 | //selecting multiple record 45 | var students []Student 46 | db.Limit(10).Find(&students) 47 | fmt.Printf("fetchStudents count: %v \n", len(students)) 48 | } 49 | -------------------------------------------------------------------------------- /go-db-comparison/examples/setup.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS students; 2 | CREATE TABLE `students` ( 3 | `id` bigint NOT NULL AUTO_INCREMENT, 4 | `fname` varchar(50) not null, 5 | `lname` varchar(50) not null, 6 | `date_of_birth` datetime not null, 7 | `email` varchar(50), 8 | `address` varchar(50), 9 | `gender` varchar(50), 10 | PRIMARY KEY (`id`) 11 | ); 12 | 13 | insert into students (id, fname, lname, date_of_birth, email, gender, address) values (1, 'Caddric', 'Likely', '2000-07-06 02:43:37', 'clikely0@wp.com', 'Male', '9173 Boyd Street'); 14 | insert into students (id, fname, lname, date_of_birth, email, gender, address) values (2, 'Jerad', 'Ciccotti', '1993-02-11 15:59:56', 'jciccotti1@bravesites.com', 'Male', '34 Declaration Drive'); 15 | insert into students (id, fname, lname, date_of_birth, email, gender, address) values (3, 'Hillier', 'Caslett', '1992-09-04 13:38:46', 'hcaslett2@hostgator.com', 'Male', '36 Duke Trail'); 16 | insert into students (id, fname, lname, date_of_birth, email, gender, address) values (4, 'Bertine', 'Roddan', '1991-02-18 09:10:05', 'broddan3@independent.co.uk', 'Female', '2896 Kropf Road'); 17 | insert into students (id, fname, lname, date_of_birth, email, gender, address) values (5, 'Theda', 'Brockton', '1991-10-29 09:08:48', 'tbrockton4@lycos.com', 'Female', '93 Hermina Plaza'); 18 | insert into students (id, fname, lname, date_of_birth, email, gender, address) values (6, 'Leon', 'Ashling', '1994-08-14 23:51:42', 'lashling5@senate.gov', 'Male', '39 Kipling Pass'); 19 | insert into students (id, fname, lname, date_of_birth, email, gender, address) values (7, 'Aldo', 'Pettitt', '1994-08-14 22:03:40', 'apettitt6@hexun.com', 'Male', '38 Dryden Road'); 20 | insert into students (id, fname, lname, date_of_birth, email, gender, address) values (8, 'Filmore', 'Cordingly', '1999-11-20 02:35:48', 'fcordingly7@163.com', 'Male', '34 Pawling Park'); 21 | insert into students (id, fname, lname, date_of_birth, email, gender, address) values (9, 'Katalin', 'MacCroary', '1994-11-08 11:59:19', 'kmaccroary8@cargocollective.com', 'Female', '2540 Maryland Parkway'); 22 | insert into students (id, fname, lname, date_of_birth, email, gender, address) values (10, 'Franky', 'Puddan', '1995-04-23 17:07:29', 'fpuddan9@psu.edu', 'Female', '3214 Washington Road'); -------------------------------------------------------------------------------- /go-db-comparison/examples/sqlc/db.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.17.2 4 | 5 | package main 6 | 7 | import ( 8 | "context" 9 | "database/sql" 10 | ) 11 | 12 | type DBTX interface { 13 | ExecContext(context.Context, string, ...interface{}) (sql.Result, error) 14 | PrepareContext(context.Context, string) (*sql.Stmt, error) 15 | QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) 16 | QueryRowContext(context.Context, string, ...interface{}) *sql.Row 17 | } 18 | 19 | func New(db DBTX) *Queries { 20 | return &Queries{db: db} 21 | } 22 | 23 | type Queries struct { 24 | db DBTX 25 | } 26 | 27 | func (q *Queries) WithTx(tx *sql.Tx) *Queries { 28 | return &Queries{ 29 | db: tx, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /go-db-comparison/examples/sqlc/models.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.17.2 4 | 5 | package main 6 | 7 | import ( 8 | "time" 9 | ) 10 | 11 | type Student struct { 12 | ID int64 13 | Fname string 14 | Lname string 15 | DateOfBirth time.Time 16 | Email string 17 | Address string 18 | Gender string 19 | } 20 | -------------------------------------------------------------------------------- /go-db-comparison/examples/sqlc/query.sql: -------------------------------------------------------------------------------- 1 | -- name: addStudent :execlastid 2 | insert into students (fname, lname, date_of_birth, email, gender, address) values (?, ?, ?, ?, ?, ?); 3 | 4 | -- name: studentByID :one 5 | SELECT * FROM students WHERE id = ?; 6 | 7 | -- name: fetchStudents :many 8 | SELECT * FROM students LIMIT 10; -------------------------------------------------------------------------------- /go-db-comparison/examples/sqlc/query.sql.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.17.2 4 | // source: query.sql 5 | 6 | package main 7 | 8 | import ( 9 | "context" 10 | "time" 11 | ) 12 | 13 | const addStudent = `-- name: addStudent :execlastid 14 | insert into students (fname, lname, date_of_birth, email, gender, address) values (?, ?, ?, ?, ?, ?) 15 | ` 16 | 17 | type addStudentParams struct { 18 | Fname string 19 | Lname string 20 | DateOfBirth time.Time 21 | Email string 22 | Gender string 23 | Address string 24 | } 25 | 26 | func (q *Queries) addStudent(ctx context.Context, arg addStudentParams) (int64, error) { 27 | result, err := q.db.ExecContext(ctx, addStudent, 28 | arg.Fname, 29 | arg.Lname, 30 | arg.DateOfBirth, 31 | arg.Email, 32 | arg.Gender, 33 | arg.Address, 34 | ) 35 | if err != nil { 36 | return 0, err 37 | } 38 | return result.LastInsertId() 39 | } 40 | 41 | const fetchStudents = `-- name: fetchStudents :many 42 | SELECT id, fname, lname, date_of_birth, email, address, gender FROM students LIMIT 10 43 | ` 44 | 45 | func (q *Queries) fetchStudents(ctx context.Context) ([]Student, error) { 46 | rows, err := q.db.QueryContext(ctx, fetchStudents) 47 | if err != nil { 48 | return nil, err 49 | } 50 | defer rows.Close() 51 | var items []Student 52 | for rows.Next() { 53 | var i Student 54 | if err := rows.Scan( 55 | &i.ID, 56 | &i.Fname, 57 | &i.Lname, 58 | &i.DateOfBirth, 59 | &i.Email, 60 | &i.Address, 61 | &i.Gender, 62 | ); err != nil { 63 | return nil, err 64 | } 65 | items = append(items, i) 66 | } 67 | if err := rows.Close(); err != nil { 68 | return nil, err 69 | } 70 | if err := rows.Err(); err != nil { 71 | return nil, err 72 | } 73 | return items, nil 74 | } 75 | 76 | const studentByID = `-- name: studentByID :one 77 | SELECT id, fname, lname, date_of_birth, email, address, gender FROM students WHERE id = ? 78 | ` 79 | 80 | func (q *Queries) studentByID(ctx context.Context, id int64) (Student, error) { 81 | row := q.db.QueryRowContext(ctx, studentByID, id) 82 | var i Student 83 | err := row.Scan( 84 | &i.ID, 85 | &i.Fname, 86 | &i.Lname, 87 | &i.DateOfBirth, 88 | &i.Email, 89 | &i.Address, 90 | &i.Gender, 91 | ) 92 | return i, err 93 | } 94 | -------------------------------------------------------------------------------- /go-db-comparison/examples/sqlc/schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `students` ( 2 | `id` bigint NOT NULL AUTO_INCREMENT, 3 | `fname` varchar(50) not null, 4 | `lname` varchar(50) not null, 5 | `date_of_birth` datetime not null, 6 | `email` varchar(50) not null, 7 | `address` varchar(50) not null, 8 | `gender` varchar(50) not null, 9 | PRIMARY KEY (`id`) 10 | ); -------------------------------------------------------------------------------- /go-db-comparison/examples/sqlc/sqlc.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "fmt" 7 | "log" 8 | "time" 9 | 10 | _ "github.com/go-sql-driver/mysql" 11 | ) 12 | 13 | func main() { 14 | conn, err := sql.Open("mysql", "theuser:thepass@tcp(localhost:3306)/thedb?parseTime=true") 15 | if err != nil { 16 | log.Fatal(err) 17 | } 18 | fmt.Println("Connected!") 19 | 20 | db := New(conn) 21 | 22 | //initializing record to be inserted 23 | newSt := addStudentParams{ 24 | Fname: "Leon", 25 | Lname: "Ashling", 26 | DateOfBirth: time.Date(1994, time.August, 14, 23, 51, 42, 0, time.UTC), 27 | Email: "lashling5@senate.gov", 28 | Gender: "Male", 29 | Address: "39 Kipling Pass", 30 | } 31 | // inserting the record 32 | sID, err := db.addStudent(context.Background(), newSt) 33 | if err != nil { 34 | log.Fatal(err) 35 | } 36 | fmt.Printf("addSudent id: %v \n", sID) 37 | 38 | //retreive record by id 39 | st, err := db.studentByID(context.Background(), sID) 40 | if err != nil { 41 | log.Println(err) 42 | } 43 | fmt.Printf("studentByID record: %v \n", st) 44 | 45 | //fetching multiple records 46 | students, err := db.fetchStudents(context.Background()) 47 | if err != nil { 48 | log.Println(err) 49 | } 50 | 51 | fmt.Printf("fetchStudents count: %v \n", len(students)) 52 | } 53 | -------------------------------------------------------------------------------- /go-db-comparison/examples/sqlc/sqlc.yaml: -------------------------------------------------------------------------------- 1 | version: 1 2 | packages: 3 | - path: "./" 4 | name: "main" 5 | engine: "mysql" 6 | schema: "schema.sql" 7 | queries: "query.sql" -------------------------------------------------------------------------------- /go-db-comparison/examples/sqlx/sqlx.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "log" 7 | "time" 8 | 9 | _ "github.com/go-sql-driver/mysql" 10 | "github.com/jmoiron/sqlx" 11 | ) 12 | 13 | type Student struct { 14 | ID int 15 | Fname string 16 | Lname string 17 | DateOfBirth time.Time `db:"date_of_birth"` 18 | Email string 19 | Address string 20 | Gender string 21 | } 22 | 23 | var db *sqlx.DB 24 | 25 | func main() { 26 | var err error 27 | db, err = sqlxConnect() 28 | if err != nil { 29 | log.Fatal(err) 30 | } 31 | 32 | s := Student{ 33 | Fname: "Leon", 34 | Lname: "Ashling", 35 | DateOfBirth: time.Date(1994, time.August, 14, 23, 51, 42, 0, time.UTC), 36 | Email: "lashling5@senate.gov", 37 | Address: "39 Kipling Pass", 38 | Gender: "Male", 39 | } 40 | 41 | //adding student record to table 42 | sID, err := addStudent(s) 43 | if err != nil { 44 | fmt.Println(err) 45 | } 46 | fmt.Printf("addStudent id: %v \n", sID) 47 | 48 | //selecting student by ID 49 | st, err := studentByID(sID) 50 | if err != nil { 51 | fmt.Println(err) 52 | } 53 | fmt.Printf("studentByID record: %v \n", st) 54 | 55 | students, err := fetchStudents() 56 | if err != nil { 57 | fmt.Println(err) 58 | } 59 | 60 | fmt.Printf("fetchStudents count: %v \n", len(students)) 61 | } 62 | 63 | func sqlxConnect() (*sqlx.DB, error) { 64 | // Opening a database connection. 65 | db, err := sqlx.Open("mysql", "theuser:thepass@tcp(localhost:3306)/thedb?parseTime=true") 66 | if err != nil { 67 | return nil, err 68 | } 69 | fmt.Println("Connected!") 70 | return db, nil 71 | } 72 | 73 | func addStudent(s Student) (int64, error) { 74 | query := "insert into students (fname, lname, date_of_birth, email, gender, address) values (?, ?, ?, ?, ?, ?);" 75 | result := db.MustExec(query, s.Fname, s.Lname, s.DateOfBirth, s.Email, s.Gender, s.Address) 76 | 77 | id, err := result.LastInsertId() 78 | if err != nil { 79 | return 0, fmt.Errorf("addSudent Error: %v", err) 80 | } 81 | 82 | return id, nil 83 | } 84 | 85 | func fetchStudents() ([]Student, error) { 86 | // A slice of Students to hold data from returned rows. 87 | var students []Student 88 | 89 | err := db.Select(&students, "SELECT * FROM students LIMIT 10") 90 | if err != nil { 91 | return nil, fmt.Errorf("fetchStudents %v", err) 92 | } 93 | 94 | return students, nil 95 | } 96 | 97 | func studentByID(id int64) (Student, error) { 98 | var st Student 99 | 100 | //if err := db.QueryRowx("SELECT * FROM students WHERE id = ?", id).StructScan(&st); err != nil { 101 | // if err == sql.ErrNoRows { 102 | // return st, fmt.Errorf("studentById %d: no such student", id) 103 | // } 104 | // return st, fmt.Errorf("studentById %d: %v", id, err) 105 | //} 106 | 107 | if err := db.Get(&st, "SELECT * FROM students WHERE id = ?", id); err != nil { 108 | if err == sql.ErrNoRows { 109 | return st, fmt.Errorf("studentById %d: no such student", id) 110 | } 111 | return st, fmt.Errorf("studentById %d: %v", id, err) 112 | } 113 | return st, nil 114 | } 115 | -------------------------------------------------------------------------------- /go-db-comparison/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/rexfordnyrk/go-db-comparison 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/go-sql-driver/mysql v1.6.0 7 | github.com/jmoiron/sqlx v1.3.5 8 | gorm.io/driver/mysql v1.4.4 9 | gorm.io/gorm v1.24.6 10 | ) 11 | -------------------------------------------------------------------------------- /go-db-comparison/go.sum: -------------------------------------------------------------------------------- 1 | github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= 2 | github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 3 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 4 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 5 | github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 6 | github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= 7 | github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 8 | github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= 9 | github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= 10 | github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= 11 | github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 12 | github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= 13 | github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= 14 | gorm.io/driver/mysql v1.4.4 h1:MX0K9Qvy0Na4o7qSC/YI7XxqUw5KDw01umqgID+svdQ= 15 | gorm.io/driver/mysql v1.4.4/go.mod h1:BCg8cKI+R0j/rZRQxeKis/forqRwRSYOR8OM3Wo6hOM= 16 | gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= 17 | gorm.io/gorm v1.24.6 h1:wy98aq9oFEetsc4CAbKD2SoBCdMzsbSIvSUUFJuHi5s= 18 | gorm.io/gorm v1.24.6/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= 19 | -------------------------------------------------------------------------------- /go-gin-react/go-gin-react-part1/go.mod: -------------------------------------------------------------------------------- 1 | module chat-app 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/gin-gonic/gin v1.9.1 7 | github.com/glebarez/go-sqlite v1.21.1 8 | ) 9 | 10 | require ( 11 | github.com/bytedance/sonic v1.9.1 // indirect 12 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect 13 | github.com/dustin/go-humanize v1.0.1 // indirect 14 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect 15 | github.com/gin-contrib/sse v0.1.0 // indirect 16 | github.com/go-playground/locales v0.14.1 // indirect 17 | github.com/go-playground/universal-translator v0.18.1 // indirect 18 | github.com/go-playground/validator/v10 v10.14.0 // indirect 19 | github.com/goccy/go-json v0.10.2 // indirect 20 | github.com/google/uuid v1.3.0 // indirect 21 | github.com/json-iterator/go v1.1.12 // indirect 22 | github.com/klauspost/cpuid/v2 v2.2.4 // indirect 23 | github.com/leodido/go-urn v1.2.4 // indirect 24 | github.com/mattn/go-isatty v0.0.19 // indirect 25 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 26 | github.com/modern-go/reflect2 v1.0.2 // indirect 27 | github.com/pelletier/go-toml/v2 v2.0.8 // indirect 28 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect 29 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 30 | github.com/ugorji/go/codec v1.2.11 // indirect 31 | golang.org/x/arch v0.3.0 // indirect 32 | golang.org/x/crypto v0.14.0 // indirect 33 | golang.org/x/net v0.17.0 // indirect 34 | golang.org/x/sys v0.13.0 // indirect 35 | golang.org/x/text v0.13.0 // indirect 36 | google.golang.org/protobuf v1.30.0 // indirect 37 | gopkg.in/yaml.v3 v3.0.1 // indirect 38 | modernc.org/libc v1.22.3 // indirect 39 | modernc.org/mathutil v1.5.0 // indirect 40 | modernc.org/memory v1.5.0 // indirect 41 | modernc.org/sqlite v1.21.1 // indirect 42 | ) 43 | -------------------------------------------------------------------------------- /go-gin-react/go-gin-react-part1/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "os" 9 | "strconv" 10 | 11 | "github.com/gin-gonic/gin" 12 | _ "github.com/glebarez/go-sqlite" 13 | ) 14 | 15 | type User struct { 16 | ID int `json:"id"` 17 | Username string `json:"username"` 18 | Password string `json:"password"` 19 | } 20 | 21 | type Channel struct { 22 | ID int `json:"id"` 23 | Name string `json:"name"` 24 | } 25 | 26 | type Message struct { 27 | ID int `json:"id"` 28 | ChannelID int `json:"channel_id"` 29 | UserID int `json:"user_id"` 30 | UserName string `json:"user_name"` 31 | Text string `json:"text"` 32 | } 33 | 34 | func main() { 35 | // Get the working directory 36 | wd, err := os.Getwd() 37 | if err != nil { 38 | log.Fatal(err) 39 | } 40 | // Print the working directory 41 | fmt.Println("Working directory:", wd) 42 | 43 | // Open the SQLite database file 44 | db, err := sql.Open("sqlite", wd+"/database.db") 45 | 46 | defer func(db *sql.DB) { 47 | err := db.Close() 48 | if err != nil { 49 | log.Fatal(err) 50 | } 51 | }(db) 52 | 53 | // Create the Gin router 54 | r := gin.Default() 55 | 56 | if err != nil { 57 | log.Fatal(err) 58 | } 59 | 60 | // Creation endpoints 61 | r.POST("/users", func(c *gin.Context) { createUser(c, db) }) 62 | r.POST("/channels", func(c *gin.Context) { createChannel(c, db) }) 63 | r.POST("/messages", func(c *gin.Context) { createMessage(c, db) }) 64 | 65 | // Listing endpoints 66 | r.GET("/channels", func(c *gin.Context) { listChannels(c, db) }) 67 | r.GET("/messages", func(c *gin.Context) { listMessages(c, db) }) 68 | 69 | // Login endpoint 70 | r.POST("/login", func(c *gin.Context) { login(c, db) }) 71 | 72 | err = r.Run(":8080") 73 | if err != nil { 74 | log.Fatal(err) 75 | } 76 | } 77 | 78 | // User creation endpoint 79 | func createUser(c *gin.Context, db *sql.DB) { 80 | // Parse JSON request body into User struct 81 | var user User 82 | if err := c.ShouldBindJSON(&user); err != nil { 83 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 84 | return 85 | } 86 | 87 | // Insert user into database 88 | result, err := db.Exec("INSERT INTO users (username, password) VALUES (?, ?)", user.Username, user.Password) 89 | if err != nil { 90 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 91 | return 92 | } 93 | 94 | // Get ID of newly inserted user 95 | id, err := result.LastInsertId() 96 | if err != nil { 97 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 98 | return 99 | } 100 | 101 | // Return ID of newly inserted user 102 | c.JSON(http.StatusOK, gin.H{"id": id}) 103 | } 104 | 105 | // Login endpoint 106 | func login(c *gin.Context, db *sql.DB) { 107 | // Parse JSON request body into User struct 108 | var user User 109 | if err := c.ShouldBindJSON(&user); err != nil { 110 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 111 | return 112 | } 113 | 114 | // Query database for user 115 | row := db.QueryRow("SELECT id FROM users WHERE username = ? AND password = ?", user.Username, user.Password) 116 | 117 | // Get ID of user 118 | var id int 119 | err := row.Scan(&id) 120 | if err != nil { 121 | // Check if user was not found 122 | if err == sql.ErrNoRows { 123 | c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid username or password"}) 124 | return 125 | } 126 | // Return error if other error occurred 127 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 128 | } 129 | 130 | // Return ID of user 131 | c.JSON(http.StatusOK, gin.H{"id": id}) 132 | } 133 | 134 | // Channel creation endpoint 135 | func createChannel(c *gin.Context, db *sql.DB) { 136 | // Parse JSON request body into Channel struct 137 | var channel Channel 138 | if err := c.ShouldBindJSON(&channel); err != nil { 139 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 140 | return 141 | } 142 | 143 | // Insert channel into database 144 | result, err := db.Exec("INSERT INTO channels (name) VALUES (?)", channel.Name) 145 | if err != nil { 146 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 147 | return 148 | } 149 | 150 | // Get ID of newly inserted channel 151 | id, err := result.LastInsertId() 152 | if err != nil { 153 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 154 | return 155 | } 156 | 157 | // Return ID of newly inserted channel 158 | c.JSON(http.StatusOK, gin.H{"id": id}) 159 | } 160 | 161 | // Channel listing endpoint 162 | func listChannels(c *gin.Context, db *sql.DB) { 163 | // Query database for channels 164 | rows, err := db.Query("SELECT id, name FROM channels") 165 | if err != nil { 166 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 167 | return 168 | } 169 | 170 | // Create slice of channels 171 | var channels []Channel 172 | 173 | // Iterate over rows 174 | for rows.Next() { 175 | // Create new channel 176 | var channel Channel 177 | 178 | // Scan row into channel 179 | err := rows.Scan(&channel.ID, &channel.Name) 180 | if err != nil { 181 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 182 | return 183 | } 184 | 185 | // Append channel to slice 186 | channels = append(channels, channel) 187 | } 188 | 189 | // Return slice of channels 190 | c.JSON(http.StatusOK, channels) 191 | } 192 | 193 | // Message creation endpoint 194 | func createMessage(c *gin.Context, db *sql.DB) { 195 | // Parse JSON request body into Message struct 196 | var message Message 197 | if err := c.ShouldBindJSON(&message); err != nil { 198 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 199 | return 200 | } 201 | 202 | // Insert message into database 203 | result, err := db.Exec("INSERT INTO messages (channel_id, user_id, message) VALUES (?, ?, ?)", message.ChannelID, message.UserID, message.Text) 204 | if err != nil { 205 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 206 | return 207 | } 208 | 209 | // Get ID of newly inserted message 210 | id, err := result.LastInsertId() 211 | if err != nil { 212 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 213 | return 214 | } 215 | 216 | // Return ID of newly inserted message 217 | c.JSON(http.StatusOK, gin.H{"id": id}) 218 | } 219 | 220 | // Message listing endpoint 221 | func listMessages(c *gin.Context, db *sql.DB) { 222 | // Parse channel ID from URL 223 | channelID, err := strconv.Atoi(c.Query("channelID")) 224 | if err != nil { 225 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 226 | return 227 | } 228 | 229 | // Parse optional limit query parameter from URL 230 | limit, err := strconv.Atoi(c.Query("limit")) 231 | if err != nil { 232 | // Set limit to 100 if not provided 233 | limit = 100 234 | } 235 | 236 | // Parse last message ID query parameter from URL. This is used to get messages after a certain message. 237 | lastMessageID, err := strconv.Atoi(c.Query("lastMessageID")) 238 | if err != nil { 239 | // Set last message ID to 0 if not provided 240 | lastMessageID = 0 241 | } 242 | 243 | // Query database for messages 244 | rows, err := db.Query("SELECT m.id, channel_id, user_id, u.username AS user_name, message FROM messages m LEFT JOIN users u ON u.id = m.user_id WHERE channel_id = ? AND m.id > ? ORDER BY m.id ASC LIMIT ?", channelID, lastMessageID, limit) 245 | if err != nil { 246 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 247 | return 248 | } 249 | 250 | // Create slice of messages 251 | var messages []Message 252 | 253 | // Iterate over rows 254 | for rows.Next() { 255 | // Create new message 256 | var message Message 257 | 258 | // Scan row into message 259 | err := rows.Scan(&message.ID, &message.ChannelID, &message.UserID, &message.UserName, &message.Text) 260 | if err != nil { 261 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 262 | return 263 | } 264 | 265 | // Append message to slice 266 | messages = append(messages, message) 267 | } 268 | 269 | // Return slice of messages 270 | c.JSON(http.StatusOK, messages) 271 | } 272 | -------------------------------------------------------------------------------- /go-gin-react/go-gin-react-part1/schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE users ( 2 | id INTEGER PRIMARY KEY, 3 | username TEXT NOT NULL, 4 | password TEXT NOT NULL 5 | ); 6 | 7 | CREATE TABLE channels ( 8 | id INTEGER PRIMARY KEY, 9 | name TEXT NOT NULL 10 | ); 11 | 12 | CREATE TABLE messages ( 13 | id INTEGER PRIMARY KEY, 14 | channel_id INTEGER NOT NULL, 15 | user_id INTEGER NOT NULL, 16 | message TEXT NOT NULL, 17 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 18 | ); -------------------------------------------------------------------------------- /go-gin-react/go-gin-react-part2/chat-ui/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /go-gin-react/go-gin-react-part2/chat-ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chat-ui", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.16.5", 7 | "@testing-library/react": "^13.4.0", 8 | "@testing-library/user-event": "^13.5.0", 9 | "react": "^18.2.0", 10 | "react-dom": "^18.2.0", 11 | "react-scripts": "5.0.1", 12 | "web-vitals": "^2.1.4" 13 | }, 14 | "scripts": { 15 | "start": "react-scripts start", 16 | "build": "react-scripts build", 17 | "test": "react-scripts test", 18 | "eject": "react-scripts eject" 19 | }, 20 | "eslintConfig": { 21 | "extends": [ 22 | "react-app", 23 | "react-app/jest" 24 | ] 25 | }, 26 | "browserslist": { 27 | "production": [ 28 | ">0.2%", 29 | "not dead", 30 | "not op_mini all" 31 | ], 32 | "development": [ 33 | "last 1 chrome version", 34 | "last 1 firefox version", 35 | "last 1 safari version" 36 | ] 37 | }, 38 | "devDependencies": { 39 | "react-router-dom": "^6.13.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /go-gin-react/go-gin-react-part2/chat-ui/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains/go-code-samples/f03be7aaaf80c54b27a10173456974ba05a18fbf/go-gin-react/go-gin-react-part2/chat-ui/public/favicon.ico -------------------------------------------------------------------------------- /go-gin-react/go-gin-react-part2/chat-ui/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 | 32 |
33 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /go-gin-react/go-gin-react-part2/chat-ui/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains/go-code-samples/f03be7aaaf80c54b27a10173456974ba05a18fbf/go-gin-react/go-gin-react-part2/chat-ui/public/logo192.png -------------------------------------------------------------------------------- /go-gin-react/go-gin-react-part2/chat-ui/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains/go-code-samples/f03be7aaaf80c54b27a10173456974ba05a18fbf/go-gin-react/go-gin-react-part2/chat-ui/public/logo512.png -------------------------------------------------------------------------------- /go-gin-react/go-gin-react-part2/chat-ui/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /go-gin-react/go-gin-react-part2/chat-ui/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /go-gin-react/go-gin-react-part2/chat-ui/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'; 3 | import Login from './Login'; 4 | import CreateUser from './CreateUser'; 5 | import MainChat from './MainChat'; 6 | 7 | const App = () => { 8 | return ( 9 | 10 | 11 | } /> 12 | } /> 13 | } /> 14 | } /> 15 | 16 | 17 | ); 18 | }; 19 | 20 | export default App; -------------------------------------------------------------------------------- /go-gin-react/go-gin-react-part2/chat-ui/src/ChannelsList.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import {useParams} from "react-router-dom"; 3 | 4 | const ChannelsList = ({ selectedChannel, setSelectedChannel }) => { 5 | const { channelId } = useParams(); 6 | const [channels, setChannels] = useState([]); 7 | const [newChannelName, setNewChannelName] = useState(''); 8 | 9 | useEffect(() => { 10 | if (channelId) { 11 | const channel = channels.find((channel) => channel.id === parseInt(channelId)); 12 | if (channel) { 13 | setSelectedChannel({name: channel.name, id: parseInt(channelId)}); 14 | } 15 | } 16 | }, [channelId, channels]); 17 | 18 | useEffect(() => { 19 | const fetchChannels = async () => { 20 | const response = await fetch('/channels'); 21 | const data = await response.json(); 22 | setChannels(data || []); 23 | }; 24 | fetchChannels(); 25 | }, []); 26 | 27 | const handleAddChannel = async () => { 28 | const response = await fetch('/channels', { 29 | method: 'POST', 30 | headers: { 'Content-Type': 'application/json' }, 31 | body: JSON.stringify({ name: newChannelName }), 32 | }); 33 | 34 | if (response.ok) { 35 | const newChannel = await response.json(); 36 | setChannels([...channels, { id: newChannel.id, name: newChannelName }]); 37 | setNewChannelName(''); 38 | } 39 | }; 40 | 41 | return ( 42 |
43 |
44 | Channels 45 |
46 |
47 | {channels ? ( 48 |
    49 | {channels.map((channel) => ( 50 |
  • setSelectedChannel(channel)} 54 | > 55 | {channel.name} 56 |
  • 57 | ))} 58 |
59 | ) : ( 60 |
61 | Please add a Channel 62 |
63 | )} 64 |
65 |
66 | setNewChannelName(e.target.value)} 70 | placeholder="New channel..." 71 | className="mb-4 p-2 w-full border rounded-md bg-white" 72 | /> 73 | 74 |
75 |
76 | ); 77 | }; 78 | 79 | export default ChannelsList; -------------------------------------------------------------------------------- /go-gin-react/go-gin-react-part2/chat-ui/src/CreateUser.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | 4 | const CreateUser = () => { 5 | const [username, setUsername] = useState(''); 6 | const [password, setPassword] = useState(''); 7 | const navigate = useNavigate(); 8 | 9 | const handleSubmit = async (e) => { 10 | e.preventDefault(); 11 | 12 | const response = await fetch('/users', { 13 | method: 'POST', 14 | headers: { 'Content-Type': 'application/json' }, 15 | body: JSON.stringify({ username, password }), 16 | }); 17 | 18 | if (response.ok) { 19 | navigate('/'); 20 | } else { 21 | alert('Account creation failed'); 22 | } 23 | }; 24 | 25 | return ( 26 |
27 |
28 |
29 | 30 | setUsername(e.target.value)} 35 | className="mt-1 p-2 w-full border rounded-md" 36 | required 37 | /> 38 |
39 |
40 | 41 | setPassword(e.target.value)} 46 | className="mt-1 p-2 w-full border rounded-md" 47 | required 48 | /> 49 |
50 | 51 |
52 |
53 | ); 54 | }; 55 | 56 | export default CreateUser; -------------------------------------------------------------------------------- /go-gin-react/go-gin-react-part2/chat-ui/src/Login.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | 4 | const Login = () => { 5 | const [username, setUsername] = useState(''); 6 | const [password, setPassword] = useState(''); 7 | const navigate = useNavigate(); 8 | 9 | const handleSubmit = async (e) => { 10 | e.preventDefault(); 11 | 12 | const response = await fetch('/login', { 13 | method: 'POST', 14 | headers: { 'Content-Type': 'application/json' }, 15 | body: JSON.stringify({ username, password }), 16 | }); 17 | 18 | if (response.ok) { 19 | const data = await response.json(); 20 | localStorage.setItem('userId', data.id); 21 | localStorage.setItem('userName', username); 22 | navigate('/chat'); 23 | } else { 24 | alert('Login failed'); 25 | } 26 | }; 27 | 28 | return ( 29 |
30 |
31 |
32 | 33 | setUsername(e.target.value)} 38 | className="mt-1 p-2 w-full border rounded-md" 39 | required 40 | /> 41 |
42 |
43 | 44 | setPassword(e.target.value)} 49 | className="mt-1 p-2 w-full border rounded-md" 50 | required 51 | /> 52 |
53 | 54 |
55 | Don't have an account? 56 | Create one 57 |
58 |
59 |
60 | ); 61 | }; 62 | 63 | export default Login; -------------------------------------------------------------------------------- /go-gin-react/go-gin-react-part2/chat-ui/src/MainChat.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { useParams, useNavigate } from 'react-router-dom'; 3 | import ChannelsList from './ChannelsList'; 4 | import MessagesPanel from './MessagesPanel'; 5 | 6 | const MainChat = () => { 7 | const { channelId } = useParams(); 8 | const navigate = useNavigate(); 9 | const [selectedChannel, setSelectedChannel] = useState(parseInt(channelId) || null); 10 | 11 | // If the component loads with a channel ID in the URL, set it as the selected channel. 12 | useEffect(() => { 13 | if (selectedChannel) { 14 | navigate(`/chat/${selectedChannel.id}`); 15 | } 16 | }, [selectedChannel, navigate]); 17 | 18 | const handleChannelSelect = (channelId) => { 19 | setSelectedChannel(channelId); 20 | }; 21 | 22 | return ( 23 |
24 |
25 | 26 |
27 |
28 | 29 |
30 |
31 | ); 32 | }; 33 | 34 | export default MainChat; -------------------------------------------------------------------------------- /go-gin-react/go-gin-react-part2/chat-ui/src/MessageEntry.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | const MessageEntry = ({ selectedChannel, onNewMessage }) => { 4 | const [text, setText] = useState(''); 5 | 6 | const handleSendMessage = async () => { 7 | const userID = localStorage.getItem('userId'); 8 | const userName = localStorage.getItem('userName'); 9 | 10 | const response = await fetch('/messages', { 11 | method: 'POST', 12 | headers: { 'Content-Type': 'application/json' }, 13 | body: JSON.stringify({ 14 | "channel_id": parseInt(selectedChannel.id), 15 | "user_id": parseInt(userID), 16 | text 17 | }), 18 | }); 19 | 20 | if (response.ok) { 21 | const message = await response.json(); 22 | onNewMessage({ 23 | id: message.id, 24 | channel_id: selectedChannel, 25 | user_id: userID, 26 | user_name: userName, 27 | text 28 | }); 29 | setText(''); 30 | } else { 31 | alert('Failed to send message'); 32 | } 33 | }; 34 | 35 | const handleKeyDown = (event) => { 36 | if (event.key === 'Enter' && !event.shiftKey) { 37 | handleSendMessage(); 38 | event.preventDefault(); // Prevent the default behavior (newline) 39 | } 40 | }; 41 | 42 | return ( 43 |
44 | setText(e.target.value)} 49 | onKeyDown={handleKeyDown} 50 | className="p-2 flex-grow border rounded-md mr-2" 51 | /> 52 | 58 |
59 | ); 60 | }; 61 | 62 | export default MessageEntry; -------------------------------------------------------------------------------- /go-gin-react/go-gin-react-part2/chat-ui/src/MessagesPanel.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from 'react'; 2 | import MessageEntry from './MessageEntry'; 3 | 4 | const MessagesPanel = ({ selectedChannel }) => { 5 | const [messages, setMessages] = useState([]); 6 | const lastMessageIdRef = useRef(null); // Keep track of the last message ID 7 | 8 | useEffect(() => { 9 | if (!selectedChannel) return; 10 | 11 | let isMounted = true; // flag to prevent state updates after unmount 12 | let intervalId = null; 13 | 14 | const fetchMessages = async () => { 15 | const response = await fetch(`/messages?channelID=${selectedChannel.id}`); 16 | const data = await response.json(); 17 | if (isMounted) { 18 | let messageData = data || []; 19 | setMessages(messageData); 20 | lastMessageIdRef.current = messageData.length > 0 ? messageData[messageData.length - 1].id : null; 21 | } 22 | }; 23 | 24 | fetchMessages(); 25 | 26 | intervalId = setInterval(() => { 27 | if (lastMessageIdRef.current !== null) { 28 | fetch(`/messages?channelID=${selectedChannel.id}&lastMessageID=${lastMessageIdRef.current}`) 29 | .then(response => response.json()) 30 | .then(newMessages => { 31 | if (isMounted && Array.isArray(newMessages) && newMessages.length > 0) { 32 | setMessages((messages) => { 33 | const updatedMessages = [...messages, ...newMessages]; 34 | lastMessageIdRef.current = updatedMessages[updatedMessages.length - 1].id; 35 | return updatedMessages; 36 | }); 37 | } 38 | }); 39 | } 40 | }, 5000); // Poll every 5 seconds 41 | 42 | return () => { 43 | isMounted = false; // prevent further state updates 44 | clearInterval(intervalId); // clear interval on unmount 45 | }; 46 | }, [selectedChannel]); 47 | 48 | return ( 49 |
50 | {selectedChannel && ( 51 |
52 | Messages for {selectedChannel.name} 53 |
54 | )} 55 |
56 | {selectedChannel ? ( 57 | messages.length > 0 ? ( 58 | messages.map((message) => ( 59 |
60 | {message.user_name}: {message.text} 61 |
62 | )) 63 | ) : ( 64 |
65 | No messages yet! Why not send one? 66 |
67 | ) 68 | ) : ( 69 |
Please select a channel
70 | )} 71 |
72 | {selectedChannel && ( 73 | { 76 | lastMessageIdRef.current = message.id; 77 | setMessages([...messages, message]) 78 | } 79 | } 80 | /> 81 | )} 82 |
83 | ); 84 | }; 85 | 86 | export default MessagesPanel; -------------------------------------------------------------------------------- /go-gin-react/go-gin-react-part2/chat-ui/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import App from './App'; 4 | 5 | const root = ReactDOM.createRoot(document.getElementById('root')); 6 | root.render( 7 | 8 | 9 | 10 | ); -------------------------------------------------------------------------------- /go-gin-react/go-gin-react-part2/database.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains/go-code-samples/f03be7aaaf80c54b27a10173456974ba05a18fbf/go-gin-react/go-gin-react-part2/database.db -------------------------------------------------------------------------------- /go-gin-react/go-gin-react-part2/go.mod: -------------------------------------------------------------------------------- 1 | module chat-app 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/gin-gonic/gin v1.9.1 7 | github.com/glebarez/go-sqlite v1.21.1 8 | ) 9 | 10 | require ( 11 | github.com/bytedance/sonic v1.9.1 // indirect 12 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect 13 | github.com/dustin/go-humanize v1.0.1 // indirect 14 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect 15 | github.com/gin-contrib/sse v0.1.0 // indirect 16 | github.com/go-playground/locales v0.14.1 // indirect 17 | github.com/go-playground/universal-translator v0.18.1 // indirect 18 | github.com/go-playground/validator/v10 v10.14.0 // indirect 19 | github.com/goccy/go-json v0.10.2 // indirect 20 | github.com/google/uuid v1.3.0 // indirect 21 | github.com/json-iterator/go v1.1.12 // indirect 22 | github.com/klauspost/cpuid/v2 v2.2.4 // indirect 23 | github.com/leodido/go-urn v1.2.4 // indirect 24 | github.com/mattn/go-isatty v0.0.19 // indirect 25 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 26 | github.com/modern-go/reflect2 v1.0.2 // indirect 27 | github.com/pelletier/go-toml/v2 v2.0.8 // indirect 28 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect 29 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 30 | github.com/ugorji/go/codec v1.2.11 // indirect 31 | golang.org/x/arch v0.3.0 // indirect 32 | golang.org/x/crypto v0.14.0 // indirect 33 | golang.org/x/net v0.17.0 // indirect 34 | golang.org/x/sys v0.13.0 // indirect 35 | golang.org/x/text v0.13.0 // indirect 36 | google.golang.org/protobuf v1.30.0 // indirect 37 | gopkg.in/yaml.v3 v3.0.1 // indirect 38 | modernc.org/libc v1.22.3 // indirect 39 | modernc.org/mathutil v1.5.0 // indirect 40 | modernc.org/memory v1.5.0 // indirect 41 | modernc.org/sqlite v1.21.1 // indirect 42 | ) 43 | -------------------------------------------------------------------------------- /go-gin-react/go-gin-react-part2/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chat-app", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "dependencies": { 8 | "react-router-dom": "^6.12.1" 9 | } 10 | }, 11 | "node_modules/@remix-run/router": { 12 | "version": "1.6.3", 13 | "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.6.3.tgz", 14 | "integrity": "sha512-EXJysQ7J3veRECd0kZFQwYYd5sJMcq2O/m60zu1W2l3oVQ9xtub8jTOtYRE0+M2iomyG/W3Ps7+vp2kna0C27Q==", 15 | "engines": { 16 | "node": ">=14" 17 | } 18 | }, 19 | "node_modules/js-tokens": { 20 | "version": "4.0.0", 21 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 22 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", 23 | "peer": true 24 | }, 25 | "node_modules/loose-envify": { 26 | "version": "1.4.0", 27 | "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", 28 | "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", 29 | "peer": true, 30 | "dependencies": { 31 | "js-tokens": "^3.0.0 || ^4.0.0" 32 | }, 33 | "bin": { 34 | "loose-envify": "cli.js" 35 | } 36 | }, 37 | "node_modules/react": { 38 | "version": "18.2.0", 39 | "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", 40 | "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", 41 | "peer": true, 42 | "dependencies": { 43 | "loose-envify": "^1.1.0" 44 | }, 45 | "engines": { 46 | "node": ">=0.10.0" 47 | } 48 | }, 49 | "node_modules/react-dom": { 50 | "version": "18.2.0", 51 | "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", 52 | "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", 53 | "peer": true, 54 | "dependencies": { 55 | "loose-envify": "^1.1.0", 56 | "scheduler": "^0.23.0" 57 | }, 58 | "peerDependencies": { 59 | "react": "^18.2.0" 60 | } 61 | }, 62 | "node_modules/react-router": { 63 | "version": "6.12.1", 64 | "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.12.1.tgz", 65 | "integrity": "sha512-evd/GrKJOeOypD0JB9e1r7pQh2gWCsTbUfq059Wm1AFT/K2MNZuDo19lFtAgIhlBrp0MmpgpqtvZC7LPAs7vSw==", 66 | "dependencies": { 67 | "@remix-run/router": "1.6.3" 68 | }, 69 | "engines": { 70 | "node": ">=14" 71 | }, 72 | "peerDependencies": { 73 | "react": ">=16.8" 74 | } 75 | }, 76 | "node_modules/react-router-dom": { 77 | "version": "6.12.1", 78 | "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.12.1.tgz", 79 | "integrity": "sha512-POIZN9UDKWwEDga054LvYr2KnK8V+0HR4Ny4Bwv8V7/FZCPxJgsCjYxXGxqxzHs7VBxMKZfgvtKhafuJkJSPGA==", 80 | "dependencies": { 81 | "@remix-run/router": "1.6.3", 82 | "react-router": "6.12.1" 83 | }, 84 | "engines": { 85 | "node": ">=14" 86 | }, 87 | "peerDependencies": { 88 | "react": ">=16.8", 89 | "react-dom": ">=16.8" 90 | } 91 | }, 92 | "node_modules/scheduler": { 93 | "version": "0.23.0", 94 | "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", 95 | "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", 96 | "peer": true, 97 | "dependencies": { 98 | "loose-envify": "^1.1.0" 99 | } 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /go-gin-react/go-gin-react-part2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "react-router-dom": "^6.12.1" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /go-gin-react/go-gin-react-part2/schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE users ( 2 | id INTEGER PRIMARY KEY, 3 | username TEXT NOT NULL, 4 | password TEXT NOT NULL 5 | ); 6 | 7 | CREATE TABLE channels ( 8 | id INTEGER PRIMARY KEY, 9 | name TEXT NOT NULL 10 | ); 11 | 12 | CREATE TABLE messages ( 13 | id INTEGER PRIMARY KEY, 14 | channel_id INTEGER NOT NULL, 15 | user_id INTEGER NOT NULL, 16 | message TEXT NOT NULL, 17 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 18 | ); -------------------------------------------------------------------------------- /go-gin-react/go-gin-react-part3/chat-ui/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /go-gin-react/go-gin-react-part3/chat-ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chat-ui", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.16.5", 7 | "@testing-library/react": "^13.4.0", 8 | "@testing-library/user-event": "^13.5.0", 9 | "http-proxy-middleware": "^2.0.6", 10 | "react": "^18.2.0", 11 | "react-dom": "^18.2.0", 12 | "react-scripts": "5.0.1", 13 | "web-vitals": "^2.1.4" 14 | }, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "react-scripts build", 18 | "test": "react-scripts test", 19 | "eject": "react-scripts eject" 20 | }, 21 | "eslintConfig": { 22 | "extends": [ 23 | "react-app", 24 | "react-app/jest" 25 | ] 26 | }, 27 | "browserslist": { 28 | "production": [ 29 | ">0.2%", 30 | "not dead", 31 | "not op_mini all" 32 | ], 33 | "development": [ 34 | "last 1 chrome version", 35 | "last 1 firefox version", 36 | "last 1 safari version" 37 | ] 38 | }, 39 | "devDependencies": { 40 | "react-router-dom": "^6.13.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /go-gin-react/go-gin-react-part3/chat-ui/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains/go-code-samples/f03be7aaaf80c54b27a10173456974ba05a18fbf/go-gin-react/go-gin-react-part3/chat-ui/public/favicon.ico -------------------------------------------------------------------------------- /go-gin-react/go-gin-react-part3/chat-ui/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 | 32 |
33 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /go-gin-react/go-gin-react-part3/chat-ui/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains/go-code-samples/f03be7aaaf80c54b27a10173456974ba05a18fbf/go-gin-react/go-gin-react-part3/chat-ui/public/logo192.png -------------------------------------------------------------------------------- /go-gin-react/go-gin-react-part3/chat-ui/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains/go-code-samples/f03be7aaaf80c54b27a10173456974ba05a18fbf/go-gin-react/go-gin-react-part3/chat-ui/public/logo512.png -------------------------------------------------------------------------------- /go-gin-react/go-gin-react-part3/chat-ui/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /go-gin-react/go-gin-react-part3/chat-ui/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /go-gin-react/go-gin-react-part3/chat-ui/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'; 3 | import Login from './Login'; 4 | import CreateUser from './CreateUser'; 5 | import MainChat from './MainChat'; 6 | 7 | const App = () => { 8 | return ( 9 | 10 | 11 | } /> 12 | } /> 13 | } /> 14 | } /> 15 | 16 | 17 | ); 18 | }; 19 | 20 | export default App; -------------------------------------------------------------------------------- /go-gin-react/go-gin-react-part3/chat-ui/src/ChannelsList.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import {useParams} from "react-router-dom"; 3 | 4 | const ChannelsList = ({ selectedChannel, setSelectedChannel }) => { 5 | const { channelId } = useParams(); 6 | const [channels, setChannels] = useState([]); 7 | const [newChannelName, setNewChannelName] = useState(''); 8 | 9 | useEffect(() => { 10 | if (channelId) { 11 | const channel = channels.find((channel) => channel.id === parseInt(channelId)); 12 | if (channel) { 13 | setSelectedChannel({name: channel.name, id: parseInt(channelId)}); 14 | } 15 | } 16 | }, [channelId, channels]); 17 | 18 | useEffect(() => { 19 | const fetchChannels = async () => { 20 | const response = await fetch('/channels'); 21 | const data = await response.json(); 22 | setChannels(data || []); 23 | }; 24 | fetchChannels(); 25 | }, []); 26 | 27 | const handleAddChannel = async () => { 28 | const response = await fetch('/channels', { 29 | method: 'POST', 30 | headers: { 'Content-Type': 'application/json' }, 31 | body: JSON.stringify({ name: newChannelName }), 32 | }); 33 | 34 | if (response.ok) { 35 | const newChannel = await response.json(); 36 | setChannels([...channels, { id: newChannel.id, name: newChannelName }]); 37 | setNewChannelName(''); 38 | } 39 | }; 40 | 41 | return ( 42 |
43 |
44 | Channels 45 |
46 |
47 | {channels ? ( 48 |
    49 | {channels.map((channel) => ( 50 |
  • setSelectedChannel(channel)} 54 | > 55 | {channel.name} 56 |
  • 57 | ))} 58 |
59 | ) : ( 60 |
61 | Please add a Channel 62 |
63 | )} 64 |
65 |
66 | setNewChannelName(e.target.value)} 70 | placeholder="New channel..." 71 | className="mb-4 p-2 w-full border rounded-md bg-white" 72 | /> 73 | 74 |
75 |
76 | ); 77 | }; 78 | 79 | export default ChannelsList; -------------------------------------------------------------------------------- /go-gin-react/go-gin-react-part3/chat-ui/src/CreateUser.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | 4 | const CreateUser = () => { 5 | const [username, setUsername] = useState(''); 6 | const [password, setPassword] = useState(''); 7 | const navigate = useNavigate(); 8 | 9 | const handleSubmit = async (e) => { 10 | e.preventDefault(); 11 | 12 | const response = await fetch('/users', { 13 | method: 'POST', 14 | headers: { 'Content-Type': 'application/json' }, 15 | body: JSON.stringify({ username, password }), 16 | }); 17 | 18 | if (response.ok) { 19 | navigate('/'); 20 | } else { 21 | alert('Account creation failed'); 22 | } 23 | }; 24 | 25 | return ( 26 |
27 |
28 |
29 | 30 | setUsername(e.target.value)} 35 | className="mt-1 p-2 w-full border rounded-md" 36 | required 37 | /> 38 |
39 |
40 | 41 | setPassword(e.target.value)} 46 | className="mt-1 p-2 w-full border rounded-md" 47 | required 48 | /> 49 |
50 | 51 |
52 |
53 | ); 54 | }; 55 | 56 | export default CreateUser; -------------------------------------------------------------------------------- /go-gin-react/go-gin-react-part3/chat-ui/src/Login.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | 4 | const Login = () => { 5 | const [username, setUsername] = useState(''); 6 | const [password, setPassword] = useState(''); 7 | const navigate = useNavigate(); 8 | 9 | const handleSubmit = async (e) => { 10 | e.preventDefault(); 11 | 12 | const response = await fetch('/login', { 13 | method: 'POST', 14 | headers: { 'Content-Type': 'application/json' }, 15 | body: JSON.stringify({ username, password }), 16 | }); 17 | 18 | if (response.ok) { 19 | const data = await response.json(); 20 | localStorage.setItem('userId', data.id); 21 | localStorage.setItem('userName', username); 22 | navigate('/chat'); 23 | } else { 24 | alert('Login failed'); 25 | } 26 | }; 27 | 28 | return ( 29 |
30 |
31 |
32 | 33 | setUsername(e.target.value)} 38 | className="mt-1 p-2 w-full border rounded-md" 39 | required 40 | /> 41 |
42 |
43 | 44 | setPassword(e.target.value)} 49 | className="mt-1 p-2 w-full border rounded-md" 50 | required 51 | /> 52 |
53 | 54 |
55 | Don't have an account? 56 | Create one 57 |
58 |
59 |
60 | ); 61 | }; 62 | 63 | export default Login; -------------------------------------------------------------------------------- /go-gin-react/go-gin-react-part3/chat-ui/src/MainChat.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { useParams, useNavigate } from 'react-router-dom'; 3 | import ChannelsList from './ChannelsList'; 4 | import MessagesPanel from './MessagesPanel'; 5 | 6 | const MainChat = () => { 7 | const { channelId } = useParams(); 8 | const navigate = useNavigate(); 9 | const [selectedChannel, setSelectedChannel] = useState(parseInt(channelId) || null); 10 | 11 | // If the component loads with a channel ID in the URL, set it as the selected channel. 12 | useEffect(() => { 13 | if (selectedChannel) { 14 | navigate(`/chat/${selectedChannel.id}`); 15 | } 16 | }, [selectedChannel, navigate]); 17 | 18 | const handleChannelSelect = (channelId) => { 19 | setSelectedChannel(channelId); 20 | }; 21 | 22 | return ( 23 |
24 |
25 | 26 |
27 |
28 | 29 |
30 |
31 | ); 32 | }; 33 | 34 | export default MainChat; -------------------------------------------------------------------------------- /go-gin-react/go-gin-react-part3/chat-ui/src/MessageEntry.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | const MessageEntry = ({ selectedChannel, onNewMessage }) => { 4 | const [text, setText] = useState(''); 5 | 6 | const handleSendMessage = async () => { 7 | const userID = localStorage.getItem('userId'); 8 | const userName = localStorage.getItem('userName'); 9 | 10 | const response = await fetch('/messages', { 11 | method: 'POST', 12 | headers: { 'Content-Type': 'application/json' }, 13 | body: JSON.stringify({ 14 | "channel_id": parseInt(selectedChannel.id), 15 | "user_id": parseInt(userID), 16 | text 17 | }), 18 | }); 19 | 20 | if (response.ok) { 21 | const message = await response.json(); 22 | onNewMessage({ 23 | id: message.id, 24 | channel_id: selectedChannel, 25 | user_id: userID, 26 | user_name: userName, 27 | text 28 | }); 29 | setText(''); 30 | } else { 31 | alert('Failed to send message'); 32 | } 33 | }; 34 | 35 | const handleKeyDown = (event) => { 36 | if (event.key === 'Enter' && !event.shiftKey) { 37 | handleSendMessage(); 38 | event.preventDefault(); // Prevent the default behavior (newline) 39 | } 40 | }; 41 | 42 | return ( 43 |
44 | setText(e.target.value)} 49 | onKeyDown={handleKeyDown} 50 | className="p-2 flex-grow border rounded-md mr-2" 51 | /> 52 | 58 |
59 | ); 60 | }; 61 | 62 | export default MessageEntry; -------------------------------------------------------------------------------- /go-gin-react/go-gin-react-part3/chat-ui/src/MessagesPanel.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from 'react'; 2 | import MessageEntry from './MessageEntry'; 3 | 4 | const MessagesPanel = ({ selectedChannel }) => { 5 | const [messages, setMessages] = useState([]); 6 | const lastMessageIdRef = useRef(null); // Keep track of the last message ID 7 | 8 | useEffect(() => { 9 | if (!selectedChannel) return; 10 | 11 | let isMounted = true; // flag to prevent state updates after unmount 12 | let intervalId = null; 13 | 14 | const fetchMessages = async () => { 15 | const response = await fetch(`/messages?channelID=${selectedChannel.id}`); 16 | const data = await response.json(); 17 | if (isMounted) { 18 | let messageData = data || []; 19 | setMessages(messageData); 20 | lastMessageIdRef.current = messageData.length > 0 ? messageData[messageData.length - 1].id : null; 21 | } 22 | }; 23 | 24 | fetchMessages(); 25 | 26 | intervalId = setInterval(() => { 27 | if (lastMessageIdRef.current !== null) { 28 | fetch(`/messages?channelID=${selectedChannel.id}&lastMessageID=${lastMessageIdRef.current}`) 29 | .then(response => response.json()) 30 | .then(newMessages => { 31 | if (isMounted && Array.isArray(newMessages) && newMessages.length > 0) { 32 | setMessages((messages) => { 33 | const updatedMessages = [...messages, ...newMessages]; 34 | lastMessageIdRef.current = updatedMessages[updatedMessages.length - 1].id; 35 | return updatedMessages; 36 | }); 37 | } 38 | }); 39 | } 40 | }, 5000); // Poll every 5 seconds 41 | 42 | return () => { 43 | isMounted = false; // prevent further state updates 44 | clearInterval(intervalId); // clear interval on unmount 45 | }; 46 | }, [selectedChannel]); 47 | 48 | return ( 49 |
50 | {selectedChannel && ( 51 |
52 | Messages for {selectedChannel.name} 53 |
54 | )} 55 |
56 | {selectedChannel ? ( 57 | messages.length > 0 ? ( 58 | messages.map((message) => ( 59 |
60 | {message.user_name}: {message.text} 61 |
62 | )) 63 | ) : ( 64 |
65 | No messages yet! Why not send one? 66 |
67 | ) 68 | ) : ( 69 |
Please select a channel
70 | )} 71 |
72 | {selectedChannel && ( 73 | { 76 | lastMessageIdRef.current = message.id; 77 | setMessages([...messages, message]) 78 | } 79 | } 80 | /> 81 | )} 82 |
83 | ); 84 | }; 85 | 86 | export default MessagesPanel; -------------------------------------------------------------------------------- /go-gin-react/go-gin-react-part3/chat-ui/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import App from './App'; 4 | 5 | const root = ReactDOM.createRoot(document.getElementById('root')); 6 | root.render( 7 | 8 | 9 | 10 | ); -------------------------------------------------------------------------------- /go-gin-react/go-gin-react-part3/chat-ui/src/setupProxy.js: -------------------------------------------------------------------------------- 1 | const { createProxyMiddleware } = require('http-proxy-middleware'); 2 | 3 | module.exports = function(app) { 4 | app.use( 5 | ["/users", "/login", "/channels", "/messages"], 6 | createProxyMiddleware({ 7 | target: 'http://localhost:8080', 8 | changeOrigin: true, 9 | }) 10 | ); 11 | }; -------------------------------------------------------------------------------- /go-gin-react/go-gin-react-part3/database.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains/go-code-samples/f03be7aaaf80c54b27a10173456974ba05a18fbf/go-gin-react/go-gin-react-part3/database.db -------------------------------------------------------------------------------- /go-gin-react/go-gin-react-part3/go.mod: -------------------------------------------------------------------------------- 1 | module chat-app 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/gin-gonic/gin v1.9.1 7 | github.com/glebarez/go-sqlite v1.21.1 8 | ) 9 | 10 | require ( 11 | github.com/bytedance/sonic v1.9.1 // indirect 12 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect 13 | github.com/dustin/go-humanize v1.0.1 // indirect 14 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect 15 | github.com/gin-contrib/sse v0.1.0 // indirect 16 | github.com/go-playground/locales v0.14.1 // indirect 17 | github.com/go-playground/universal-translator v0.18.1 // indirect 18 | github.com/go-playground/validator/v10 v10.14.0 // indirect 19 | github.com/goccy/go-json v0.10.2 // indirect 20 | github.com/google/uuid v1.3.0 // indirect 21 | github.com/json-iterator/go v1.1.12 // indirect 22 | github.com/klauspost/cpuid/v2 v2.2.4 // indirect 23 | github.com/leodido/go-urn v1.2.4 // indirect 24 | github.com/mattn/go-isatty v0.0.19 // indirect 25 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 26 | github.com/modern-go/reflect2 v1.0.2 // indirect 27 | github.com/pelletier/go-toml/v2 v2.0.8 // indirect 28 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect 29 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 30 | github.com/ugorji/go/codec v1.2.11 // indirect 31 | golang.org/x/arch v0.3.0 // indirect 32 | golang.org/x/crypto v0.14.0 // indirect 33 | golang.org/x/net v0.17.0 // indirect 34 | golang.org/x/sys v0.13.0 // indirect 35 | golang.org/x/text v0.13.0 // indirect 36 | google.golang.org/protobuf v1.30.0 // indirect 37 | gopkg.in/yaml.v3 v3.0.1 // indirect 38 | modernc.org/libc v1.22.3 // indirect 39 | modernc.org/mathutil v1.5.0 // indirect 40 | modernc.org/memory v1.5.0 // indirect 41 | modernc.org/sqlite v1.21.1 // indirect 42 | ) 43 | -------------------------------------------------------------------------------- /go-gin-react/go-gin-react-part3/schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE users ( 2 | id INTEGER PRIMARY KEY, 3 | username TEXT NOT NULL, 4 | password TEXT NOT NULL 5 | ); 6 | 7 | CREATE TABLE channels ( 8 | id INTEGER PRIMARY KEY, 9 | name TEXT NOT NULL 10 | ); 11 | 12 | CREATE TABLE messages ( 13 | id INTEGER PRIMARY KEY, 14 | channel_id INTEGER NOT NULL, 15 | user_id INTEGER NOT NULL, 16 | message TEXT NOT NULL, 17 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 18 | ); -------------------------------------------------------------------------------- /go-rest-demo/README.md: -------------------------------------------------------------------------------- 1 | # Go REST Guide 2 | 3 | This is a three-part series dedicated to three different methods of building a Go REST API. 4 | 5 | Find the tutorial [here](https://www.jetbrains.com/go/guide/tutorials/rest_api_series/). -------------------------------------------------------------------------------- /go-rest-demo/cmd/gin/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/gosimple/slug" 8 | "github.com/xNok/go-rest-demo/pkg/recipes" 9 | ) 10 | 11 | func main() { 12 | // Create Gin router 13 | router := gin.Default() 14 | 15 | // Instantiate recipe Handler and provide a data store 16 | store := recipes.NewMemStore() 17 | recipesHandler := NewRecipesHandler(store) 18 | 19 | // Register Routes 20 | router.GET("/", homePage) 21 | router.GET("/recipes", recipesHandler.ListRecipes) 22 | router.POST("/recipes", recipesHandler.CreateRecipe) 23 | router.GET("/recipes/:id", recipesHandler.GetRecipe) 24 | router.PUT("/recipes/:id", recipesHandler.UpdateRecipe) 25 | router.DELETE("/recipes/:id", recipesHandler.DeleteRecipe) 26 | 27 | // Start the server 28 | router.Run() 29 | } 30 | 31 | func homePage(c *gin.Context) { 32 | c.String(http.StatusOK, "This is my home page") 33 | } 34 | 35 | type RecipesHandler struct { 36 | store recipeStore 37 | } 38 | 39 | func NewRecipesHandler(s recipeStore) *RecipesHandler { 40 | return &RecipesHandler{ 41 | store: s, 42 | } 43 | } 44 | 45 | type recipeStore interface { 46 | Add(name string, recipe recipes.Recipe) error 47 | Get(name string) (recipes.Recipe, error) 48 | List() (map[string]recipes.Recipe, error) 49 | Update(name string, recipe recipes.Recipe) error 50 | Remove(name string) error 51 | } 52 | 53 | func (h RecipesHandler) CreateRecipe(c *gin.Context) { 54 | // Get request body and convert it to recipes.Recipe 55 | var recipe recipes.Recipe 56 | if err := c.ShouldBindJSON(&recipe); err != nil { 57 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 58 | return 59 | } 60 | 61 | // create a url friendly name 62 | id := slug.Make(recipe.Name) 63 | 64 | // add to the store 65 | h.store.Add(id, recipe) 66 | 67 | // return success payload 68 | c.JSON(http.StatusOK, gin.H{"status": "success"}) 69 | } 70 | 71 | func (h RecipesHandler) ListRecipes(c *gin.Context) { 72 | r, err := h.store.List() 73 | if err != nil { 74 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 75 | } 76 | 77 | c.JSON(200, r) 78 | } 79 | 80 | func (h RecipesHandler) GetRecipe(c *gin.Context) { 81 | id := c.Param("id") 82 | 83 | recipe, err := h.store.Get(id) 84 | if err != nil { 85 | c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) 86 | } 87 | 88 | c.JSON(200, recipe) 89 | } 90 | 91 | func (h RecipesHandler) UpdateRecipe(c *gin.Context) { 92 | // Get request body and convert it to recipes.Recipe 93 | var recipe recipes.Recipe 94 | if err := c.ShouldBindJSON(&recipe); err != nil { 95 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 96 | return 97 | } 98 | 99 | id := c.Param("id") 100 | 101 | err := h.store.Update(id, recipe) 102 | if err != nil { 103 | if err == recipes.NotFoundErr { 104 | c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) 105 | return 106 | } 107 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 108 | return 109 | } 110 | 111 | // return success payload 112 | c.JSON(http.StatusOK, gin.H{"status": "success"}) 113 | } 114 | 115 | func (h RecipesHandler) DeleteRecipe(c *gin.Context) { 116 | id := c.Param("id") 117 | 118 | err := h.store.Remove(id) 119 | if err != nil { 120 | if err == recipes.NotFoundErr { 121 | c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) 122 | return 123 | } 124 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 125 | return 126 | } 127 | 128 | // return success payload 129 | c.JSON(http.StatusOK, gin.H{"status": "success"}) 130 | 131 | } 132 | -------------------------------------------------------------------------------- /go-rest-demo/cmd/gin/main_test.http: -------------------------------------------------------------------------------- 1 | ### 2 | GET http://localhost:8080/ 3 | 4 | ### 5 | POST http://localhost:8080/recipes 6 | Content-Type: application/json 7 | 8 | { 9 | "name": "Ham and cheese toasties", 10 | "ingredients": [ 11 | { 12 | "name": "bread" 13 | },{ 14 | "name": "ham" 15 | },{ 16 | "name": "cheese" 17 | } 18 | ] 19 | } 20 | 21 | > {% 22 | client.test("Request executed successfully", function() { 23 | client.assert(response.status === 200, "Response status is not 200"); 24 | }); 25 | %} 26 | 27 | ### 28 | GET http://localhost:8080/recipes 29 | 30 | > {% 31 | client.test("Request executed successfully", function() { 32 | client.assert(response.status === 200, "Response status is not 200"); 33 | client.assert(JSON.stringify(response.body) === "{\"ham-and-cheese-toasties\":{\"name\":\"Ham and cheese toasties\",\"ingredients\":[{\"name\":\"bread\"},{\"name\":\"ham\"},{\"name\":\"cheese\"}]}}", "Body match expected response") 34 | }); 35 | %} 36 | 37 | ### 38 | GET http://localhost:8080/recipes/ham-and-cheese-toasties 39 | 40 | > {% 41 | client.test("Request executed successfully", function() { 42 | client.assert(response.status === 200, "Response status is not 200"); 43 | client.assert(JSON.stringify(response.body) === "{\"name\":\"Ham and cheese toasties\",\"ingredients\":[{\"name\":\"bread\"},{\"name\":\"ham\"},{\"name\":\"cheese\"}]}", "Body match expected response") 44 | }); 45 | %} 46 | 47 | ### 48 | PUT http://localhost:8080/recipes/ham-and-cheese-toasties 49 | Content-Type: application/json 50 | 51 | { 52 | "name": "Ham and cheese toasties", 53 | "ingredients": [ 54 | { 55 | "name": "bread" 56 | },{ 57 | "name": "ham" 58 | },{ 59 | "name": "cheese" 60 | },{ 61 | "name": "butter" 62 | } 63 | ] 64 | } 65 | 66 | > {% 67 | client.test("Request executed successfully", function() { 68 | client.assert(response.status === 200, "Response status is not 200"); 69 | }); 70 | %} 71 | 72 | ### 73 | GET http://localhost:8080/recipes/ham-and-cheese-toasties 74 | 75 | > {% 76 | client.test("Request executed successfully", function() { 77 | client.assert(response.status === 200, "Response status is not 200"); 78 | client.assert(JSON.stringify(response.body) === "{\"name\":\"Ham and cheese toasties\",\"ingredients\":[{\"name\":\"bread\"},{\"name\":\"ham\"},{\"name\":\"cheese\"},{\"name\":\"butter\"}]}", "Body match expected response") 79 | 80 | }); 81 | %} 82 | 83 | ### 84 | DELETE http://localhost:8080/recipes/ham-and-cheese-toasties 85 | 86 | > {% 87 | client.test("Request executed successfully", function() { 88 | client.assert(response.status === 200, "Response status is not 200"); 89 | }); 90 | %} 91 | 92 | ### 93 | GET http://localhost:8080/recipes/ham-and-cheese-toasties 94 | 95 | > {% 96 | client.test("Request executed successfully", function() { 97 | client.assert(response.status === 404, "Response status is not 404"); 98 | }); 99 | %} -------------------------------------------------------------------------------- /go-rest-demo/cmd/gorilla/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | "github.com/gorilla/mux" 8 | "github.com/gosimple/slug" 9 | "github.com/xNok/go-rest-demo/pkg/recipes" 10 | ) 11 | 12 | func main() { 13 | // create the Store and Recipe Handler 14 | store := recipes.NewMemStore() 15 | recipesHandler := NewRecipesHandler(store) 16 | home := homeHandler{} 17 | 18 | router := mux.NewRouter() 19 | 20 | router.HandleFunc("/", home.ServeHTTP) 21 | router.HandleFunc("/recipes", recipesHandler.ListRecipes).Methods("GET") 22 | router.HandleFunc("/recipes", recipesHandler.CreateRecipe).Methods("POST") 23 | router.HandleFunc("/recipes/{id}", recipesHandler.GetRecipe).Methods("GET") 24 | router.HandleFunc("/recipes/{id}", recipesHandler.UpdateRecipe).Methods("PUT") 25 | router.HandleFunc("/recipes/{id}", recipesHandler.DeleteRecipe).Methods("DELETE") 26 | 27 | http.ListenAndServe(":8010", router) 28 | } 29 | 30 | func InternalServerErrorHandler(w http.ResponseWriter, r *http.Request) { 31 | w.WriteHeader(http.StatusInternalServerError) 32 | w.Write([]byte("500 Internal Server Error")) 33 | } 34 | 35 | func NotFoundHandler(w http.ResponseWriter, r *http.Request) { 36 | w.WriteHeader(http.StatusNotFound) 37 | w.Write([]byte("404 Not Found")) 38 | } 39 | 40 | type RecipesHandler struct { 41 | store recipeStore 42 | } 43 | 44 | func NewRecipesHandler(s recipeStore) *RecipesHandler { 45 | return &RecipesHandler{ 46 | store: s, 47 | } 48 | } 49 | 50 | type recipeStore interface { 51 | Add(name string, recipe recipes.Recipe) error 52 | Get(name string) (recipes.Recipe, error) 53 | List() (map[string]recipes.Recipe, error) 54 | Update(name string, recipe recipes.Recipe) error 55 | Remove(name string) error 56 | } 57 | 58 | func (h *RecipesHandler) CreateRecipe(w http.ResponseWriter, r *http.Request) { 59 | // Recipe object that will be populated from json payload 60 | var recipe recipes.Recipe 61 | 62 | if err := json.NewDecoder(r.Body).Decode(&recipe); err != nil { 63 | InternalServerErrorHandler(w, r) 64 | return 65 | } 66 | 67 | resourceID := slug.Make(recipe.Name) 68 | 69 | if err := h.store.Add(resourceID, recipe); err != nil { 70 | InternalServerErrorHandler(w, r) 71 | return 72 | } 73 | 74 | w.WriteHeader(http.StatusOK) 75 | } 76 | 77 | func (h *RecipesHandler) ListRecipes(w http.ResponseWriter, r *http.Request) { 78 | recipes, err := h.store.List() 79 | 80 | jsonBytes, err := json.Marshal(recipes) 81 | if err != nil { 82 | InternalServerErrorHandler(w, r) 83 | return 84 | } 85 | 86 | w.WriteHeader(http.StatusOK) 87 | w.Write(jsonBytes) 88 | } 89 | 90 | func (h *RecipesHandler) GetRecipe(w http.ResponseWriter, r *http.Request) { 91 | id := mux.Vars(r)["id"] 92 | 93 | recipe, err := h.store.Get(id) 94 | if err != nil { 95 | if err == recipes.NotFoundErr { 96 | NotFoundHandler(w, r) 97 | return 98 | } 99 | 100 | InternalServerErrorHandler(w, r) 101 | return 102 | } 103 | 104 | jsonBytes, err := json.Marshal(recipe) 105 | if err != nil { 106 | InternalServerErrorHandler(w, r) 107 | return 108 | } 109 | 110 | w.WriteHeader(http.StatusOK) 111 | w.Write(jsonBytes) 112 | } 113 | 114 | func (h *RecipesHandler) UpdateRecipe(w http.ResponseWriter, r *http.Request) { 115 | id := mux.Vars(r)["id"] 116 | 117 | // Recipe object that will be populated from json payload 118 | var recipe recipes.Recipe 119 | if err := json.NewDecoder(r.Body).Decode(&recipe); err != nil { 120 | InternalServerErrorHandler(w, r) 121 | return 122 | } 123 | 124 | if err := h.store.Update(id, recipe); err != nil { 125 | if err == recipes.NotFoundErr { 126 | NotFoundHandler(w, r) 127 | return 128 | } 129 | 130 | InternalServerErrorHandler(w, r) 131 | return 132 | } 133 | 134 | jsonBytes, err := json.Marshal(recipe) 135 | if err != nil { 136 | InternalServerErrorHandler(w, r) 137 | return 138 | } 139 | 140 | w.WriteHeader(http.StatusOK) 141 | w.Write(jsonBytes) 142 | } 143 | 144 | func (h *RecipesHandler) DeleteRecipe(w http.ResponseWriter, r *http.Request) { 145 | id := mux.Vars(r)["id"] 146 | 147 | if err := h.store.Remove(id); err != nil { 148 | InternalServerErrorHandler(w, r) 149 | return 150 | } 151 | 152 | w.WriteHeader(http.StatusOK) 153 | } 154 | 155 | type homeHandler struct{} 156 | 157 | func (h *homeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 158 | w.Write([]byte("This is my home page")) 159 | } 160 | -------------------------------------------------------------------------------- /go-rest-demo/cmd/gorilla/main_test.http: -------------------------------------------------------------------------------- 1 | ### 2 | POST http://localhost:8010/recipes 3 | Content-Type: application/json 4 | 5 | { 6 | "name": "Ham and cheese toasties", 7 | "ingredients": [ 8 | { 9 | "name": "bread" 10 | },{ 11 | "name": "ham" 12 | },{ 13 | "name": "cheese" 14 | } 15 | ] 16 | } 17 | 18 | > {% 19 | client.test("Request executed successfully", function() { 20 | client.assert(response.status === 200, "Response status is not 200"); 21 | }); 22 | %} 23 | 24 | ### 25 | GET http://localhost:8010/recipes 26 | 27 | > {% 28 | client.test("Request executed successfully", function() { 29 | client.assert(response.status === 200, "Response status is not 200"); 30 | client.assert(response.body === "{\"ham-and-cheese-toasties\":{\"name\":\"Ham and cheese toasties\",\"ingredients\":[{\"name\":\"bread\"},{\"name\":\"ham\"},{\"name\":\"cheese\"}]}}", "Body match expected response") 31 | }); 32 | %} 33 | 34 | ### 35 | GET http://localhost:8010/recipes/ham-and-cheese-toasties 36 | 37 | > {% 38 | client.test("Request executed successfully", function() { 39 | client.assert(response.status === 200, "Response status is not 200"); 40 | client.assert(response.body === "{\"name\":\"Ham and cheese toasties\",\"ingredients\":[{\"name\":\"bread\"},{\"name\":\"ham\"},{\"name\":\"cheese\"}]}", "Body match expected response") 41 | }); 42 | %} 43 | 44 | ### 45 | PUT http://localhost:8010/recipes/ham-and-cheese-toasties 46 | Content-Type: application/json 47 | 48 | { 49 | "name": "Ham and cheese toasties", 50 | "ingredients": [ 51 | { 52 | "name": "bread" 53 | },{ 54 | "name": "ham" 55 | },{ 56 | "name": "cheese" 57 | },{ 58 | "name": "butter" 59 | } 60 | ] 61 | } 62 | 63 | > {% 64 | client.test("Request executed successfully", function() { 65 | client.assert(response.status === 200, "Response status is not 200"); 66 | }); 67 | %} 68 | 69 | ### 70 | GET http://localhost:8010/recipes/ham-and-cheese-toasties 71 | 72 | > {% 73 | client.test("Request executed successfully", function() { 74 | client.assert(response.status === 200, "Response status is not 200"); 75 | client.assert(response.body === "{\"name\":\"Ham and cheese toasties\",\"ingredients\":[{\"name\":\"bread\"},{\"name\":\"ham\"},{\"name\":\"cheese\"},{\"name\":\"butter\"}]}", "Body match expected response") 76 | 77 | }); 78 | %} 79 | 80 | ### 81 | DELETE http://localhost:8010/recipes/ham-and-cheese-toasties 82 | 83 | > {% 84 | client.test("Request executed successfully", function() { 85 | client.assert(response.status === 200, "Response status is not 200"); 86 | }); 87 | %} 88 | 89 | ### 90 | GET http://localhost:8010/recipes/ham-and-cheese-toasties 91 | 92 | > {% 93 | client.test("Request executed successfully", function() { 94 | client.assert(response.status === 404, "Response status is not 404"); 95 | }); 96 | %} -------------------------------------------------------------------------------- /go-rest-demo/cmd/standardlib/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "regexp" 7 | 8 | "github.com/gosimple/slug" 9 | "github.com/xNok/go-rest-demo/pkg/recipes" 10 | ) 11 | 12 | var ( 13 | RecipeRe = regexp.MustCompile(`^/recipes/*$`) 14 | RecipeReWithID = regexp.MustCompile(`^/recipes/([a-z0-9]+(?:-[a-z0-9]+)+)$`) 15 | ) 16 | 17 | func main() { 18 | 19 | // create the Store and Recipe Handler 20 | store := recipes.NewMemStore() 21 | recipesHandler := NewRecipesHandler(store) 22 | 23 | // Create a new request multiplexer 24 | // Takes incoming requests and dispatch them to the matching handlers 25 | mux := http.NewServeMux() 26 | 27 | // Register the routes and handlers 28 | mux.Handle("/", &homeHandler{}) 29 | mux.Handle("/recipes", recipesHandler) 30 | mux.Handle("/recipes/", recipesHandler) 31 | 32 | // Run the server 33 | http.ListenAndServe(":8080", mux) 34 | } 35 | 36 | type homeHandler struct{} 37 | 38 | func (h *homeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 39 | w.Write([]byte("This is my home page")) 40 | } 41 | 42 | func InternalServerErrorHandler(w http.ResponseWriter, r *http.Request) { 43 | w.WriteHeader(http.StatusInternalServerError) 44 | w.Write([]byte("500 Internal Server Error")) 45 | } 46 | 47 | func NotFoundHandler(w http.ResponseWriter, r *http.Request) { 48 | w.WriteHeader(http.StatusNotFound) 49 | w.Write([]byte("404 Not Found")) 50 | } 51 | 52 | // recipeStore represent a data storage containing recipes 53 | type recipeStore interface { 54 | Add(name string, recipe recipes.Recipe) error 55 | Get(name string) (recipes.Recipe, error) 56 | List() (map[string]recipes.Recipe, error) 57 | Update(name string, recipe recipes.Recipe) error 58 | Remove(name string) error 59 | } 60 | 61 | // RecipesHandler implements http.Handler and dispatch request to the store 62 | type RecipesHandler struct { 63 | store recipeStore 64 | } 65 | 66 | func NewRecipesHandler(s recipeStore) *RecipesHandler { 67 | return &RecipesHandler{ 68 | store: s, 69 | } 70 | } 71 | 72 | func (h *RecipesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 73 | switch { 74 | case r.Method == http.MethodPost && RecipeRe.MatchString(r.URL.Path): 75 | h.CreateRecipe(w, r) 76 | return 77 | case r.Method == http.MethodGet && RecipeRe.MatchString(r.URL.Path): 78 | h.ListRecipes(w, r) 79 | return 80 | case r.Method == http.MethodGet && RecipeReWithID.MatchString(r.URL.Path): 81 | h.GetRecipe(w, r) 82 | return 83 | case r.Method == http.MethodPut && RecipeReWithID.MatchString(r.URL.Path): 84 | h.UpdateRecipe(w, r) 85 | return 86 | case r.Method == http.MethodDelete && RecipeReWithID.MatchString(r.URL.Path): 87 | h.DeleteRecipe(w, r) 88 | return 89 | default: 90 | NotFoundHandler(w, r) 91 | return 92 | } 93 | } 94 | 95 | func (h *RecipesHandler) CreateRecipe(w http.ResponseWriter, r *http.Request) { 96 | // Recipe object that will be populated from json payload 97 | var recipe recipes.Recipe 98 | 99 | if err := json.NewDecoder(r.Body).Decode(&recipe); err != nil { 100 | InternalServerErrorHandler(w, r) 101 | return 102 | } 103 | 104 | resourceID := slug.Make(recipe.Name) 105 | 106 | if err := h.store.Add(resourceID, recipe); err != nil { 107 | InternalServerErrorHandler(w, r) 108 | return 109 | } 110 | 111 | w.WriteHeader(http.StatusOK) 112 | } 113 | 114 | func (h *RecipesHandler) ListRecipes(w http.ResponseWriter, r *http.Request) { 115 | recipesList, err := h.store.List() 116 | 117 | jsonBytes, err := json.Marshal(recipesList) 118 | if err != nil { 119 | InternalServerErrorHandler(w, r) 120 | return 121 | } 122 | 123 | w.WriteHeader(http.StatusOK) 124 | w.Write(jsonBytes) 125 | } 126 | 127 | func (h *RecipesHandler) GetRecipe(w http.ResponseWriter, r *http.Request) { 128 | matches := RecipeReWithID.FindStringSubmatch(r.URL.Path) 129 | if len(matches) < 2 { 130 | InternalServerErrorHandler(w, r) 131 | return 132 | } 133 | 134 | recipe, err := h.store.Get(matches[1]) 135 | if err != nil { 136 | if err == recipes.NotFoundErr { 137 | NotFoundHandler(w, r) 138 | return 139 | } 140 | 141 | InternalServerErrorHandler(w, r) 142 | return 143 | } 144 | 145 | jsonBytes, err := json.Marshal(recipe) 146 | if err != nil { 147 | InternalServerErrorHandler(w, r) 148 | return 149 | } 150 | 151 | w.WriteHeader(http.StatusOK) 152 | w.Write(jsonBytes) 153 | } 154 | 155 | func (h *RecipesHandler) UpdateRecipe(w http.ResponseWriter, r *http.Request) { 156 | matches := RecipeReWithID.FindStringSubmatch(r.URL.Path) 157 | if len(matches) < 2 { 158 | InternalServerErrorHandler(w, r) 159 | return 160 | } 161 | 162 | // Recipe object that will be populated from JSON payload 163 | var recipe recipes.Recipe 164 | if err := json.NewDecoder(r.Body).Decode(&recipe); err != nil { 165 | InternalServerErrorHandler(w, r) 166 | return 167 | } 168 | 169 | if err := h.store.Update(matches[1], recipe); err != nil { 170 | if err == recipes.NotFoundErr { 171 | NotFoundHandler(w, r) 172 | return 173 | } 174 | InternalServerErrorHandler(w, r) 175 | return 176 | } 177 | 178 | w.WriteHeader(http.StatusOK) 179 | } 180 | 181 | func (h *RecipesHandler) DeleteRecipe(w http.ResponseWriter, r *http.Request) { 182 | matches := RecipeReWithID.FindStringSubmatch(r.URL.Path) 183 | if len(matches) < 2 { 184 | InternalServerErrorHandler(w, r) 185 | return 186 | } 187 | 188 | if err := h.store.Remove(matches[1]); err != nil { 189 | InternalServerErrorHandler(w, r) 190 | return 191 | } 192 | 193 | w.WriteHeader(http.StatusOK) 194 | } 195 | -------------------------------------------------------------------------------- /go-rest-demo/cmd/standardlib/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "net/http" 7 | "net/http/httptest" 8 | "os" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | "github.com/xNok/go-rest-demo/pkg/recipes" 13 | ) 14 | 15 | func readTestData(t *testing.T, name string) []byte { 16 | t.Helper() 17 | content, err := os.ReadFile("../../testdata/" + name) 18 | if err != nil { 19 | t.Errorf("Could not read %v", name) 20 | } 21 | 22 | return content 23 | } 24 | 25 | func TestRecipesHandlerCRUD_Integration(t *testing.T) { 26 | 27 | // Create a MemStore and Recipe Handler 28 | store := recipes.NewMemStore() 29 | recipesHandler := NewRecipesHandler(store) 30 | 31 | // Testdata 32 | hamAndCheese := readTestData(t, "ham_and_cheese_recipe.json") 33 | hamAndCheeseReader := bytes.NewReader(hamAndCheese) 34 | 35 | hamAndCheeseWithButter := readTestData(t, "ham_and_cheese_with_butter_recipe.json") 36 | hamAndCheeseWithButterReader := bytes.NewReader(hamAndCheeseWithButter) 37 | 38 | // CREATE - add a new recipe 39 | req := httptest.NewRequest(http.MethodPost, "/recipes", hamAndCheeseReader) 40 | w := httptest.NewRecorder() 41 | recipesHandler.ServeHTTP(w, req) 42 | 43 | res := w.Result() 44 | defer res.Body.Close() 45 | assert.Equal(t, 200, res.StatusCode) 46 | 47 | saved, _ := store.List() 48 | assert.Len(t, saved, 1) 49 | 50 | // GET - find the record we just added 51 | req = httptest.NewRequest(http.MethodGet, "/recipes/ham-and-cheese-toasties", nil) 52 | w = httptest.NewRecorder() 53 | recipesHandler.ServeHTTP(w, req) 54 | 55 | res = w.Result() 56 | defer res.Body.Close() 57 | assert.Equal(t, 200, res.StatusCode) 58 | 59 | data, err := io.ReadAll(res.Body) 60 | if err != nil { 61 | t.Errorf("unexpected error: %v", err) 62 | } 63 | 64 | assert.JSONEq(t, string(hamAndCheese), string(data)) 65 | 66 | // UPDATE - add butter to ham and cheese recipe 67 | req = httptest.NewRequest(http.MethodPut, "/recipes/ham-and-cheese-toasties", hamAndCheeseWithButterReader) 68 | w = httptest.NewRecorder() 69 | recipesHandler.ServeHTTP(w, req) 70 | 71 | res = w.Result() 72 | defer res.Body.Close() 73 | assert.Equal(t, 200, res.StatusCode) 74 | 75 | updatedHamAndCheese, err := store.Get("ham-and-cheese-toasties") 76 | assert.NoError(t, err) 77 | 78 | assert.Contains(t, updatedHamAndCheese.Ingredients, recipes.Ingredient{Name: "butter"}) 79 | 80 | //DELETE - remove the ham and cheese recipe 81 | req = httptest.NewRequest(http.MethodDelete, "/recipes/ham-and-cheese-toasties", nil) 82 | w = httptest.NewRecorder() 83 | recipesHandler.ServeHTTP(w, req) 84 | 85 | res = w.Result() 86 | defer res.Body.Close() 87 | assert.Equal(t, 200, res.StatusCode) 88 | 89 | saved, _ = store.List() 90 | assert.Len(t, saved, 0) 91 | 92 | } 93 | -------------------------------------------------------------------------------- /go-rest-demo/cmd/standardlib/main_test.http: -------------------------------------------------------------------------------- 1 | ### 2 | GET http://localhost:8080 3 | 4 | ### 5 | POST http://localhost:8080/recipes/ 6 | Content-Type: application/json 7 | 8 | { 9 | "name": "Ham and cheese toasties", 10 | "ingredients": [ 11 | { 12 | "name": "bread" 13 | },{ 14 | "name": "ham" 15 | },{ 16 | "name": "cheese" 17 | } 18 | ] 19 | } 20 | 21 | ### 22 | GET http://localhost:8080/recipes 23 | 24 | ### 25 | GET http://localhost:8080/recipes/ham-and-cheese-toasties 26 | 27 | ### 28 | PUT http://localhost:8080/recipes/ham-and-cheese-toasties 29 | Content-Type: application/json 30 | 31 | { 32 | "name": "Ham and cheese toasties", 33 | "ingredients": [ 34 | { 35 | "name": "bread" 36 | },{ 37 | "name": "ham" 38 | },{ 39 | "name": "cheese" 40 | },{ 41 | "name": "butter" 42 | } 43 | ] 44 | } 45 | 46 | ### 47 | GET http://localhost:8080/recipes/ham-and-cheese-toasties 48 | 49 | ### 50 | DELETE http://localhost:8080/recipes/ham-and-cheese-toasties 51 | 52 | ### 53 | GET http://localhost:8080/recipes/ham-and-cheese-toasties -------------------------------------------------------------------------------- /go-rest-demo/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/xNok/go-rest-demo 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/gin-gonic/gin v1.9.1 7 | github.com/gorilla/mux v1.8.0 8 | github.com/gosimple/slug v1.13.1 9 | github.com/stretchr/testify v1.8.3 10 | ) 11 | 12 | require ( 13 | github.com/bytedance/sonic v1.9.1 // indirect 14 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect 15 | github.com/davecgh/go-spew v1.1.1 // indirect 16 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect 17 | github.com/gin-contrib/sse v0.1.0 // indirect 18 | github.com/go-playground/locales v0.14.1 // indirect 19 | github.com/go-playground/universal-translator v0.18.1 // indirect 20 | github.com/go-playground/validator/v10 v10.14.0 // indirect 21 | github.com/goccy/go-json v0.10.2 // indirect 22 | github.com/gosimple/unidecode v1.0.1 // indirect 23 | github.com/json-iterator/go v1.1.12 // indirect 24 | github.com/klauspost/cpuid/v2 v2.2.4 // indirect 25 | github.com/kr/pretty v0.3.0 // indirect 26 | github.com/leodido/go-urn v1.2.4 // indirect 27 | github.com/mattn/go-isatty v0.0.19 // indirect 28 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 29 | github.com/modern-go/reflect2 v1.0.2 // indirect 30 | github.com/pelletier/go-toml/v2 v2.0.8 // indirect 31 | github.com/pmezard/go-difflib v1.0.0 // indirect 32 | github.com/rogpeppe/go-internal v1.8.0 // indirect 33 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 34 | github.com/ugorji/go/codec v1.2.11 // indirect 35 | golang.org/x/arch v0.3.0 // indirect 36 | golang.org/x/crypto v0.14.0 // indirect 37 | golang.org/x/net v0.17.0 // indirect 38 | golang.org/x/sys v0.13.0 // indirect 39 | golang.org/x/text v0.13.0 // indirect 40 | google.golang.org/protobuf v1.30.0 // indirect 41 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 42 | gopkg.in/yaml.v3 v3.0.1 // indirect 43 | ) 44 | -------------------------------------------------------------------------------- /go-rest-demo/pkg/recipes/models.go: -------------------------------------------------------------------------------- 1 | package recipes 2 | 3 | // represents a recipe 4 | type Recipe struct { 5 | Name string `json:"name"` 6 | Ingredients []Ingredient `json:"ingredients"` 7 | } 8 | 9 | // represents individual ingredients 10 | type Ingredient struct { 11 | Name string `json:"name"` 12 | } 13 | -------------------------------------------------------------------------------- /go-rest-demo/pkg/recipes/recipeMemStore.go: -------------------------------------------------------------------------------- 1 | package recipes 2 | 3 | import "errors" 4 | 5 | var ( 6 | NotFoundErr = errors.New("not found") 7 | ) 8 | 9 | type MemStore struct { 10 | list map[string]Recipe 11 | } 12 | 13 | func NewMemStore() *MemStore { 14 | list := make(map[string]Recipe) 15 | return &MemStore{ 16 | list, 17 | } 18 | } 19 | 20 | func (m MemStore) Add(name string, recipe Recipe) error { 21 | m.list[name] = recipe 22 | return nil 23 | } 24 | 25 | func (m MemStore) Get(name string) (Recipe, error) { 26 | 27 | if val, ok := m.list[name]; ok { 28 | return val, nil 29 | } 30 | 31 | return Recipe{}, NotFoundErr 32 | } 33 | 34 | func (m MemStore) List() (map[string]Recipe, error) { 35 | return m.list, nil 36 | } 37 | 38 | func (m MemStore) Update(name string, recipe Recipe) error { 39 | 40 | if _, ok := m.list[name]; ok { 41 | m.list[name] = recipe 42 | return nil 43 | } 44 | 45 | return NotFoundErr 46 | } 47 | 48 | func (m MemStore) Remove(name string) error { 49 | delete(m.list, name) 50 | return nil 51 | } 52 | -------------------------------------------------------------------------------- /go-rest-demo/pkg/recipes/recipeMemStore_test.go: -------------------------------------------------------------------------------- 1 | package recipes 2 | 3 | import ( 4 | "fmt" 5 | "github.com/stretchr/testify/assert" 6 | "github.com/stretchr/testify/require" 7 | "testing" 8 | ) 9 | 10 | func getHamCheeseToasties() Recipe { 11 | return Recipe{ 12 | Name: "ham and cheese toastie", 13 | Ingredients: []Ingredient{ 14 | {Name: "bread"}, 15 | {Name: "ham"}, 16 | {Name: "cheese"}, 17 | }, 18 | } 19 | } 20 | 21 | func TestMemStore_Add(t *testing.T) { 22 | type fields struct { 23 | list map[string]Recipe 24 | } 25 | type args struct { 26 | name string 27 | recipe Recipe 28 | } 29 | tests := []struct { 30 | name string 31 | fields fields 32 | args args 33 | wantErr bool 34 | wantLen int 35 | }{ 36 | { 37 | name: "Add to empty map", 38 | fields: fields{ 39 | map[string]Recipe{}, 40 | }, 41 | args: args{ 42 | name: "ham and cheese toastie", 43 | recipe: getHamCheeseToasties(), 44 | }, 45 | wantLen: 1, 46 | wantErr: false, 47 | }, 48 | } 49 | for _, tt := range tests { 50 | t.Run(tt.name, func(t *testing.T) { 51 | m := MemStore{ 52 | list: tt.fields.list, 53 | } 54 | err := m.Add(tt.args.name, tt.args.recipe) 55 | if !tt.wantErr { 56 | assert.NoError(t, err) 57 | } 58 | 59 | assert.Len(t, tt.fields.list, tt.wantLen) 60 | }) 61 | } 62 | } 63 | 64 | func TestMemStore_Get(t *testing.T) { 65 | type fields struct { 66 | list map[string]Recipe 67 | } 68 | type args struct { 69 | name string 70 | } 71 | tests := []struct { 72 | name string 73 | fields fields 74 | args args 75 | want Recipe 76 | wantErr assert.ErrorAssertionFunc 77 | }{ 78 | { 79 | name: "Find Ham and cheese toasties", 80 | fields: fields{ 81 | map[string]Recipe{ 82 | "Ham and cheese toasties": getHamCheeseToasties(), 83 | }, 84 | }, 85 | args: args{ 86 | name: "Ham and cheese toasties", 87 | }, 88 | want: getHamCheeseToasties(), 89 | wantErr: nil, 90 | }, 91 | { 92 | name: "Not Found Ratatouille", 93 | fields: fields{ 94 | map[string]Recipe{ 95 | "Ham and cheese toasties": getHamCheeseToasties(), 96 | }, 97 | }, 98 | args: args{ 99 | name: "Ratatouille", 100 | }, 101 | want: Recipe{}, 102 | wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { 103 | return err == NotFoundErr 104 | }, 105 | }, 106 | } 107 | for _, tt := range tests { 108 | t.Run(tt.name, func(t *testing.T) { 109 | m := MemStore{ 110 | list: tt.fields.list, 111 | } 112 | got, err := m.Get(tt.args.name) 113 | if tt.wantErr != nil { 114 | if !tt.wantErr(t, err, fmt.Sprintf("Get(%v)", tt.args.name)) { 115 | require.Failf(t, "Invalid error message", "Got: %v", err.Error()) 116 | } 117 | } else { 118 | assert.NoError(t, err) 119 | } 120 | 121 | assert.Equalf(t, tt.want, got, "Get(%v)", tt.args.name) 122 | }) 123 | } 124 | } 125 | 126 | func TestMemStore_List(t *testing.T) { 127 | type fields struct { 128 | list map[string]Recipe 129 | } 130 | tests := []struct { 131 | name string 132 | fields fields 133 | want map[string]Recipe 134 | wantErr assert.ErrorAssertionFunc 135 | }{ 136 | { 137 | name: "Simple list", 138 | fields: fields{ 139 | map[string]Recipe{ 140 | "Ham and cheese toasties": getHamCheeseToasties(), 141 | }, 142 | }, 143 | want: map[string]Recipe{ 144 | "Ham and cheese toasties": getHamCheeseToasties(), 145 | }, 146 | wantErr: nil, 147 | }, 148 | } 149 | for _, tt := range tests { 150 | t.Run(tt.name, func(t *testing.T) { 151 | m := MemStore{ 152 | list: tt.fields.list, 153 | } 154 | got, err := m.List() 155 | if tt.wantErr != nil { 156 | if !tt.wantErr(t, err, fmt.Sprintf("List()")) { 157 | assert.Fail(t, "Invalid error") 158 | } 159 | } else { 160 | assert.NoError(t, err) 161 | } 162 | 163 | assert.Equalf(t, tt.want, got, "List()") 164 | }) 165 | } 166 | } 167 | 168 | func TestMemStore_Remove(t *testing.T) { 169 | type fields struct { 170 | list map[string]Recipe 171 | } 172 | type args struct { 173 | name string 174 | } 175 | tests := []struct { 176 | name string 177 | fields fields 178 | args args 179 | wantErr assert.ErrorAssertionFunc 180 | wantLen int 181 | }{ 182 | { 183 | name: "Empty list", 184 | fields: fields{ 185 | map[string]Recipe{ 186 | "Ham and cheese toasties": getHamCheeseToasties(), 187 | }, 188 | }, 189 | args: args{ 190 | name: "Ham and cheese toasties", 191 | }, 192 | wantErr: nil, 193 | }, 194 | } 195 | for _, tt := range tests { 196 | t.Run(tt.name, func(t *testing.T) { 197 | m := MemStore{ 198 | list: tt.fields.list, 199 | } 200 | 201 | err := m.Remove(tt.args.name) 202 | 203 | if tt.wantErr != nil { 204 | if !tt.wantErr(t, err, fmt.Sprintf("List()")) { 205 | assert.Fail(t, "Invalid error") 206 | } 207 | } else { 208 | assert.NoError(t, err) 209 | } 210 | 211 | assert.Len(t, m.list, tt.wantLen) 212 | }) 213 | } 214 | } 215 | 216 | func TestMemStore_Update(t *testing.T) { 217 | type fields struct { 218 | list map[string]Recipe 219 | } 220 | type args struct { 221 | name string 222 | recipe Recipe 223 | } 224 | tests := []struct { 225 | name string 226 | fields fields 227 | args args 228 | wantErr assert.ErrorAssertionFunc 229 | wantLen int 230 | }{ 231 | { 232 | name: "Update butter to Ham and cheese", 233 | fields: fields{ 234 | map[string]Recipe{ 235 | "Ham and cheese toasties": getHamCheeseToasties(), 236 | }, 237 | }, 238 | args: args{ 239 | name: "Ham and cheese toasties", 240 | recipe: Recipe{ 241 | Name: "Ham and cheese toasties", 242 | Ingredients: []Ingredient{ 243 | {Name: "bread"}, 244 | {Name: "ham"}, 245 | {Name: "cheese"}, 246 | {Name: "butter"}, 247 | }, 248 | }, 249 | }, 250 | wantErr: nil, 251 | wantLen: 1, 252 | }, 253 | } 254 | for _, tt := range tests { 255 | t.Run(tt.name, func(t *testing.T) { 256 | m := MemStore{ 257 | list: tt.fields.list, 258 | } 259 | 260 | err := m.Update(tt.args.name, tt.args.recipe) 261 | if tt.wantErr != nil { 262 | if !tt.wantErr(t, err, fmt.Sprintf("List()")) { 263 | assert.Fail(t, "Invalid error") 264 | } 265 | } else { 266 | assert.NoError(t, err) 267 | } 268 | 269 | assert.Len(t, m.list, tt.wantLen) 270 | }) 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /go-rest-demo/testdata/ham_and_cheese_recipe.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Ham and cheese toasties", 3 | "ingredients": [ 4 | { 5 | "name": "bread" 6 | },{ 7 | "name": "ham" 8 | },{ 9 | "name": "cheese" 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /go-rest-demo/testdata/ham_and_cheese_with_butter_recipe.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Ham and cheese toasties", 3 | "ingredients": [ 4 | { 5 | "name": "bread" 6 | },{ 7 | "name": "ham" 8 | },{ 9 | "name": "cheese" 10 | },{ 11 | "name": "butter" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /mock-testing/README.md: -------------------------------------------------------------------------------- 1 | # How to Use Mock Testing in Go 2 | 3 | This tutorial demonstrates several techniques for using mock objects in Go tests. 4 | 5 | Find the tutorial [here](). 6 | -------------------------------------------------------------------------------- /mock-testing/fetchuser/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /mock-testing/fetchuser/.idea/fetchuser.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /mock-testing/fetchuser/.idea/runConfigurations/TestProcessUser_All.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /mock-testing/fetchuser/.idea/runConfigurations/TestProcessUser_HigherOrderFunctions.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /mock-testing/fetchuser/.idea/runConfigurations/TestProcessUser_HttpTest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /mock-testing/fetchuser/.idea/runConfigurations/TestProcessUser_InterfaceMock1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /mock-testing/fetchuser/.idea/runConfigurations/TestProcessUser_InterfaceMock2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /mock-testing/fetchuser/.idea/runConfigurations/TestProcessUser_Mockgen.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /mock-testing/fetchuser/.idea/runConfigurations/TestProcessUser_TestifyMock.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /mock-testing/fetchuser/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /mock-testing/fetchuser/fetchuser.go: -------------------------------------------------------------------------------- 1 | package fetchuser 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | ) 9 | 10 | type User struct { 11 | ID int `json:"id"` 12 | Name string `json:"name"` 13 | } 14 | 15 | type APIFetcher interface { 16 | FetchData(id int) (User, error) 17 | } 18 | 19 | type RealAPIFetcher struct { 20 | ApiURL string 21 | } 22 | 23 | func (ra *RealAPIFetcher) FetchData(id int) (User, error) { 24 | resp, err := http.Get(fmt.Sprintf("%s/users/%d", ra.ApiURL, id)) 25 | if err != nil { 26 | return User{}, err 27 | } 28 | defer resp.Body.Close() 29 | 30 | bodyBytes, err := io.ReadAll(resp.Body) 31 | if err != nil { 32 | return User{}, err 33 | } 34 | 35 | var user User 36 | err = json.Unmarshal(bodyBytes, &user) 37 | return user, err 38 | } 39 | 40 | func ProcessUser(fetcher APIFetcher, id int) (User, error) { 41 | user, err := fetcher.FetchData(id) 42 | if err != nil { 43 | return User{}, err 44 | } 45 | // Process the user data. 46 | return user, nil 47 | } 48 | -------------------------------------------------------------------------------- /mock-testing/fetchuser/fetchuser_higherorderfunctions.go: -------------------------------------------------------------------------------- 1 | package fetchuser 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | ) 9 | 10 | type FetchDataFunc func(url string, id int) (User, error) 11 | 12 | func RealFetchData(url string, id int) (User, error) { 13 | resp, err := http.Get(fmt.Sprintf("%s/users/%d", url, id)) 14 | if err != nil { 15 | return User{}, err 16 | } 17 | defer resp.Body.Close() 18 | 19 | bodyBytes, err := io.ReadAll(resp.Body) 20 | if err != nil { 21 | return User{}, err 22 | } 23 | 24 | var user User 25 | err = json.Unmarshal(bodyBytes, &user) 26 | return user, err 27 | } 28 | 29 | func ProcessUserHOF(fetchData FetchDataFunc, url string, id int) (User, error) { 30 | user, err := fetchData(url, id) 31 | if err != nil { 32 | return User{}, err 33 | } 34 | // Process the user data. 35 | return user, nil 36 | } 37 | -------------------------------------------------------------------------------- /mock-testing/fetchuser/fetchuser_higherorderfunctions_test.go: -------------------------------------------------------------------------------- 1 | package fetchuser 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestProcessUser_HigherOrderFunctions(t *testing.T) { 8 | user := User{ID: 1, Name: "Alice"} 9 | 10 | var mockFetcher FetchDataFunc = func(url string, id int) (User, error) { 11 | return user, nil 12 | } 13 | 14 | result, err := ProcessUserHOF(mockFetcher, "noURL", 1) 15 | if err != nil { 16 | t.Errorf("Unexpected error: %v", err) 17 | } 18 | 19 | if result != user { 20 | t.Errorf("Expected user: %v, got: %v", user, result) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /mock-testing/fetchuser/fetchuser_httptest_test.go: -------------------------------------------------------------------------------- 1 | package fetchuser 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | ) 9 | 10 | func TestProcessUser_HttpTest(t *testing.T) { 11 | user := User{ID: 1, Name: "Alice"} 12 | 13 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 14 | userJSON, _ := json.Marshal(user) 15 | n, err := w.Write(userJSON) 16 | if err != nil { 17 | t.Errorf("test server: unexpected error after writing %d bytes: %v", n, err) 18 | } 19 | })) 20 | defer ts.Close() 21 | 22 | fetcher := &RealAPIFetcher{ 23 | ApiURL: ts.URL, // mock URL provided by httptest 24 | } 25 | http.DefaultClient = ts.Client() // client provided by httptest 26 | 27 | result, err := ProcessUser(fetcher, 1) 28 | if err != nil { 29 | t.Errorf("unexpected error: %v", err) 30 | } 31 | 32 | if result != user { 33 | t.Errorf("expected user: %v, got: %v", user, result) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /mock-testing/fetchuser/fetchuser_interface_test.go: -------------------------------------------------------------------------------- 1 | package fetchuser 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | type MockInterfaceFetcher struct { 8 | u User 9 | } 10 | 11 | func (m *MockInterfaceFetcher) FetchData(_ int) (User, error) { 12 | return m.u, nil 13 | } 14 | 15 | func TestProcessUser_InterfaceMock(t *testing.T) { 16 | user := User{ID: 1, Name: "Alice"} 17 | result, err := ProcessUser(&MockInterfaceFetcher{user}, 1) 18 | if err != nil { 19 | t.Errorf("Unexpected error: %v", err) 20 | } 21 | 22 | if result != user { 23 | t.Errorf("Expected user: %v, got: %v", user, result) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /mock-testing/fetchuser/fetchuser_mockgen_mocks.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: fetchuser.go 3 | 4 | // Package fetchuser is a generated GoMock package. 5 | package fetchuser 6 | 7 | import ( 8 | reflect "reflect" 9 | 10 | gomock "go.uber.org/mock/gomock" 11 | ) 12 | 13 | // MockAPIFetcher is a mock of APIFetcher interface. 14 | type MockAPIFetcher struct { 15 | ctrl *gomock.Controller 16 | recorder *MockAPIFetcherMockRecorder 17 | } 18 | 19 | // MockAPIFetcherMockRecorder is the mock recorder for MockAPIFetcher. 20 | type MockAPIFetcherMockRecorder struct { 21 | mock *MockAPIFetcher 22 | } 23 | 24 | // NewMockAPIFetcher creates a new mock instance. 25 | func NewMockAPIFetcher(ctrl *gomock.Controller) *MockAPIFetcher { 26 | mock := &MockAPIFetcher{ctrl: ctrl} 27 | mock.recorder = &MockAPIFetcherMockRecorder{mock} 28 | return mock 29 | } 30 | 31 | // EXPECT returns an object that allows the caller to indicate expected use. 32 | func (m *MockAPIFetcher) EXPECT() *MockAPIFetcherMockRecorder { 33 | return m.recorder 34 | } 35 | 36 | // FetchData mocks base method. 37 | func (m *MockAPIFetcher) FetchData(id int) (User, error) { 38 | m.ctrl.T.Helper() 39 | ret := m.ctrl.Call(m, "FetchData", id) 40 | ret0, _ := ret[0].(User) 41 | ret1, _ := ret[1].(error) 42 | return ret0, ret1 43 | } 44 | 45 | // FetchData indicates an expected call of FetchData. 46 | func (mr *MockAPIFetcherMockRecorder) FetchData(id interface{}) *gomock.Call { 47 | mr.mock.ctrl.T.Helper() 48 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchData", reflect.TypeOf((*MockAPIFetcher)(nil).FetchData), id) 49 | } 50 | -------------------------------------------------------------------------------- /mock-testing/fetchuser/fetchuser_mockgen_test.go: -------------------------------------------------------------------------------- 1 | package fetchuser 2 | 3 | import ( 4 | "testing" 5 | 6 | "go.uber.org/mock/gomock" 7 | ) 8 | 9 | func TestProcessUser_Mockgen(t *testing.T) { 10 | ctrl := gomock.NewController(t) 11 | defer ctrl.Finish() 12 | 13 | user := User{ID: 1, Name: "Alice"} 14 | mockFetcher := NewMockAPIFetcher(ctrl) 15 | mockFetcher.EXPECT().FetchData(1).Return(user, nil) 16 | 17 | result, err := ProcessUser(mockFetcher, 1) 18 | if err != nil { 19 | t.Errorf("unexpected error: %v", err) 20 | } 21 | 22 | if result != user { 23 | t.Errorf("expected user: %v, got: %v", user, result) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /mock-testing/fetchuser/fetchuser_testify_test.go: -------------------------------------------------------------------------------- 1 | package fetchuser 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/mock" 7 | ) 8 | 9 | type MockTestifyFetcher struct { 10 | mock.Mock 11 | } 12 | 13 | func (m *MockTestifyFetcher) FetchData(id int) (User, error) { 14 | args := m.Called(id) 15 | return args.Get(0).(User), args.Error(1) 16 | } 17 | 18 | func TestProcessUser_TestifyMock(t *testing.T) { 19 | user := User{ID: 1, Name: "Alice"} 20 | mockFetcher := new(MockTestifyFetcher) 21 | mockFetcher.On("FetchData", 1).Return(user, nil) 22 | 23 | result, err := ProcessUser(mockFetcher, 1) 24 | if err != nil { 25 | t.Errorf("unexpected error: %v", err) 26 | } 27 | 28 | if result != user { 29 | t.Errorf("expected user: %v, got: %v", user, result) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /mock-testing/fetchuser/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/JetBrains/go-code-samples/mock-testing/fetchuser 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/stretchr/testify v1.8.4 7 | go.uber.org/mock v0.2.0 8 | ) 9 | 10 | require ( 11 | github.com/davecgh/go-spew v1.1.1 // indirect 12 | github.com/pmezard/go-difflib v1.0.0 // indirect 13 | github.com/stretchr/objx v0.5.0 // indirect 14 | gopkg.in/yaml.v3 v3.0.1 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /mock-testing/fetchuser/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 5 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 6 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 7 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 8 | github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= 9 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 10 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 11 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 12 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 13 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 14 | go.uber.org/mock v0.2.0 h1:TaP3xedm7JaAgScZO7tlvlKrqT0p7I6OsdGB5YNSMDU= 15 | go.uber.org/mock v0.2.0/go.mod h1:J0y0rp9L3xiff1+ZBfKxlC1fz2+aO16tw0tsDOixfuM= 16 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 17 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 18 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 19 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 20 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 21 | -------------------------------------------------------------------------------- /testing-guide/README.md: -------------------------------------------------------------------------------- 1 | # Comprehensive Guide to Testing in Go 2 | 3 | This article will cover everything you need to know about Go testing. You will start with a simple testing function, and work through more tools and strategies to help you master testing in Go. 4 | 5 | Find the tutorial [here](https://blog.jetbrains.com/go/2022/11/22/comprehensive-guide-to-testing-in-go/). -------------------------------------------------------------------------------- /testing-guide/fooer.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "strconv" 4 | 5 | // Fooer If the number is divisible by 3, write "Foo" otherwise, the number 6 | func Fooer(input int) string { 7 | 8 | isfoo := (input % 3) == 0 9 | 10 | if isfoo { 11 | return "Foo" 12 | } 13 | 14 | return strconv.Itoa(input) 15 | } 16 | -------------------------------------------------------------------------------- /testing-guide/fooer_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "github.com/stretchr/testify/require" 6 | "testing" 7 | ) 8 | 9 | func TestFooer(t *testing.T) { 10 | result := Fooer(3) 11 | if result != "Foo" { 12 | t.Errorf("Result was incorrect, got: %s, want: %s.", result, "Foo") 13 | } 14 | } 15 | 16 | func TestFooer1(t *testing.T) { 17 | type args struct { 18 | input int 19 | } 20 | tests := []struct { 21 | name string 22 | args args 23 | want string 24 | }{ 25 | {"9 should be Foo", args{9}, "Foo"}, 26 | {"3 should be Foo", args{3}, "Foo"}, 27 | {"1 is not Foo", args{1}, "1"}, 28 | {"0 should be Foo", args{0}, "Foo"}, 29 | } 30 | for _, tt := range tests { 31 | t.Run(tt.name, func(t *testing.T) { 32 | if got := Fooer(tt.args.input); got != tt.want { 33 | t.Errorf("Fooer() = %v, want %v", got, tt.want) 34 | } 35 | }) 36 | } 37 | } 38 | 39 | func TestFooer2(t *testing.T) { 40 | input := 3 41 | result := Fooer(3) 42 | 43 | t.Logf("The input was %d", input) 44 | 45 | if result != "Foo" { 46 | t.Errorf("Result was incorrect, got: %s, want: %s.", result, "Foo") 47 | } 48 | 49 | t.Fatalf("Stop the test now, we have seen enough") 50 | 51 | t.Error("This won't be executed") 52 | } 53 | 54 | func TestFooerParallel(t *testing.T) { 55 | t.Run("Test 3 in Parallel", func(t *testing.T) { 56 | t.Parallel() 57 | result := Fooer(3) 58 | if result != "Foo" { 59 | t.Errorf("Result was incorrect, got: %s, want: %s.", result, "Foo") 60 | } 61 | }) 62 | 63 | t.Run("Test 7 in Parallel", func(t *testing.T) { 64 | t.Parallel() 65 | result := Fooer(7) 66 | if result != "7" { 67 | t.Errorf("Result was incorrect, got: %s, want: %s.", result, "7") 68 | } 69 | }) 70 | } 71 | 72 | func TestFooerSkipped(t *testing.T) { 73 | if testing.Short() { 74 | t.Skip("skipping test in short mode.") 75 | } 76 | 77 | result := Fooer(3) 78 | if result != "Foo" { 79 | t.Errorf("Result was incorrect, got: %s, want: %s.", result, "Foo") 80 | } 81 | } 82 | 83 | func BenchmarkFooer(b *testing.B) { 84 | for i := 0; i < b.N; i++ { 85 | Fooer(i) 86 | } 87 | } 88 | 89 | func TestFooerWithTestify(t *testing.T) { 90 | 91 | // assert equality 92 | assert.Equal(t, "Foo", Fooer(0), "0 is divisible by 3, should return Foo") 93 | 94 | // assert inequality 95 | assert.NotEqual(t, "Foo", Fooer(1), "1 is not divisible by 3, should not return Foo") 96 | } 97 | 98 | func TestMapWithTestify(t *testing.T) { 99 | 100 | // require equality 101 | require.Equal(t, map[int]string{1: "1", 2: "2"}, map[int]string{1: "1", 2: "3"}) 102 | 103 | // assert equality 104 | assert.Equal(t, map[int]string{1: "1", 2: "2"}, map[int]string{1: "1", 2: "2"}) 105 | } 106 | -------------------------------------------------------------------------------- /testing-guide/go.mod: -------------------------------------------------------------------------------- 1 | module testing-guide 2 | 3 | go 1.19 4 | 5 | require github.com/stretchr/testify v1.8.1 6 | 7 | require ( 8 | github.com/davecgh/go-spew v1.1.1 // indirect 9 | github.com/pmezard/go-difflib v1.0.0 // indirect 10 | github.com/stretchr/objx v0.5.0 // indirect 11 | gopkg.in/yaml.v3 v3.0.1 // indirect 12 | ) 13 | -------------------------------------------------------------------------------- /testing-guide/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 5 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 6 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 7 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 8 | github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= 9 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 10 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 11 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 12 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 13 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 14 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 15 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 16 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 17 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 18 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 19 | --------------------------------------------------------------------------------