Added plan of tests
This commit is contained in:
parent
a889e1c7dd
commit
640a6d2a46
36
go-code-samples/CODE_OF_CONDUCT.md
Normal file
36
go-code-samples/CODE_OF_CONDUCT.md
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# Code of Conduct
|
||||
|
||||
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.
|
||||
|
||||
## How to behave
|
||||
|
||||
The following behaviours are expected and requested of all community members:
|
||||
|
||||
- Participate in an authentic and active way. In doing so, you contribute to the health and longevity of this community.
|
||||
- Exercise consideration, respect and empathy in your speech and actions. Remember, we have all been through different stages of learning when adopting technologies.
|
||||
- Refrain from demeaning, discriminatory, or harassing behaviour and speech.
|
||||
- Disagreements on things are fine, argumentative behaviour or trolling are not.
|
||||
|
||||
## How not to behave
|
||||
|
||||
- Do not perform threats of violence or use violent language directed against another person.
|
||||
- Do not make jokes of sexist, racist, homophobic, transphobic, ableist or otherwise discriminatory nature, or use language of this nature.
|
||||
- Do not post or display sexually explicit or violent material.
|
||||
- Do not post or threaten to post other people's personally identifying information ("doxing").
|
||||
- Do not make personal insults, particularly those related to gender, sexual orientation, race, religion, or disability.
|
||||
- Do not engage in sexual attention. This includes, sexualised comments or jokes and sexual advances.
|
||||
- Do not advocate for, or encourage, any of the above behaviour.
|
||||
|
||||
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.
|
||||
|
||||
## How to report issues
|
||||
|
||||
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 <codeofconduct@jetbrains.com>
|
||||
|
||||
## Consequences of Unacceptable Behaviour
|
||||
|
||||
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.
|
||||
|
||||
## License and attribution
|
||||
|
||||
The license is based off of The Citizen Code of Conduct is distributed by Stumptown Syndicate under a Creative Commons Attribution-ShareAlike license.
|
||||
6
go-code-samples/GoBlog/README.md
Normal file
6
go-code-samples/GoBlog/README.md
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# Blog with Go Templates
|
||||
|
||||
This is a blog with templates. It uses the `html/template` package to generate web pages.
|
||||
It uses the Chi router to set up the routes and an SQLite database to store the articles.
|
||||
|
||||
Find the tutorial [here](https://blog.jetbrains.com/go/2022/11/08/build-a-blog-with-go-templates/).
|
||||
BIN
go-code-samples/GoBlog/data.sqlite
Normal file
BIN
go-code-samples/GoBlog/data.sqlite
Normal file
Binary file not shown.
117
go-code-samples/GoBlog/db.go
Normal file
117
go-code-samples/GoBlog/db.go
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
package main
|
||||
|
||||
import "database/sql"
|
||||
|
||||
func connect() (*sql.DB, error) {
|
||||
var err error
|
||||
db, err = sql.Open("sqlite3", "./data.sqlite")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sqlStmt := `
|
||||
create table if not exists articles (id integer not null primary key autoincrement, title text, content text);
|
||||
`
|
||||
|
||||
_, err = db.Exec(sqlStmt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func dbCreateArticle(article *Article) error {
|
||||
query, err := db.Prepare("insert into articles(title,content) values (?,?)")
|
||||
defer query.Close()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = query.Exec(article.Title, article.Content)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func dbGetAllArticles() ([]*Article, error) {
|
||||
query, err := db.Prepare("select id, title, content from articles")
|
||||
defer query.Close()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result, err := query.Query()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
articles := make([]*Article, 0)
|
||||
for result.Next() {
|
||||
data := new(Article)
|
||||
err := result.Scan(
|
||||
&data.ID,
|
||||
&data.Title,
|
||||
&data.Content,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
articles = append(articles, data)
|
||||
}
|
||||
|
||||
return articles, nil
|
||||
}
|
||||
|
||||
func dbGetArticle(articleID string) (*Article, error) {
|
||||
query, err := db.Prepare("select id, title, content from articles where id = ?")
|
||||
defer query.Close()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := query.QueryRow(articleID)
|
||||
data := new(Article)
|
||||
err = result.Scan(&data.ID, &data.Title, &data.Content)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func dbUpdateArticle(id string, article *Article) error {
|
||||
query, err := db.Prepare("update articles set (title, content) = (?,?) where id=?")
|
||||
defer query.Close()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = query.Exec(article.Title, article.Content, id)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func dbDeleteArticle(id string) error {
|
||||
query, err := db.Prepare("delete from articles where id=?")
|
||||
defer query.Close()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = query.Exec(id)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
8
go-code-samples/GoBlog/go.mod
Normal file
8
go-code-samples/GoBlog/go.mod
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
module GoBlog
|
||||
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/go-chi/chi/v5 v5.0.7
|
||||
github.com/mattn/go-sqlite3 v1.14.15
|
||||
)
|
||||
4
go-code-samples/GoBlog/go.sum
Normal file
4
go-code-samples/GoBlog/go.sum
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8=
|
||||
github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
|
||||
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
BIN
go-code-samples/GoBlog/images/1662832742598308748.jpeg
Normal file
BIN
go-code-samples/GoBlog/images/1662832742598308748.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
221
go-code-samples/GoBlog/main.go
Normal file
221
go-code-samples/GoBlog/main.go
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"html/template"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
var router *chi.Mux
|
||||
var db *sql.DB
|
||||
|
||||
type Article struct {
|
||||
ID int `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Content template.HTML `json:"content"`
|
||||
}
|
||||
|
||||
func catch(err error) {
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
router = chi.NewRouter()
|
||||
router.Use(middleware.Recoverer)
|
||||
|
||||
var err error
|
||||
db, err = connect()
|
||||
catch(err)
|
||||
}
|
||||
|
||||
func main() {
|
||||
router = chi.NewRouter()
|
||||
router.Use(middleware.Recoverer)
|
||||
|
||||
var err error
|
||||
db, err = connect()
|
||||
catch(err)
|
||||
|
||||
router.Use(ChangeMethod)
|
||||
router.Get("/", GetAllArticles)
|
||||
router.Post("/upload", UploadHandler) // Add this
|
||||
router.Get("/images/*", ServeImages) // Add this
|
||||
router.Route("/articles", func(r chi.Router) {
|
||||
r.Get("/", NewArticle)
|
||||
r.Post("/", CreateArticle)
|
||||
r.Route("/{articleID}", func(r chi.Router) {
|
||||
r.Use(ArticleCtx)
|
||||
r.Get("/", GetArticle) // GET /articles/1234
|
||||
r.Put("/", UpdateArticle) // PUT /articles/1234
|
||||
r.Delete("/", DeleteArticle) // DELETE /articles/1234
|
||||
r.Get("/edit", EditArticle) // GET /articles/1234/edit
|
||||
})
|
||||
})
|
||||
|
||||
err = http.ListenAndServe(":8005", router)
|
||||
catch(err)
|
||||
}
|
||||
|
||||
func UploadHandler(w http.ResponseWriter, r *http.Request) {
|
||||
const MAX_UPLOAD_SIZE = 10 << 20
|
||||
r.Body = http.MaxBytesReader(w, r.Body, MAX_UPLOAD_SIZE)
|
||||
if err := r.ParseMultipartForm(MAX_UPLOAD_SIZE); err != nil {
|
||||
http.Error(w, "The uploaded file is too big. Please choose an file that's less than 10MB in size", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
file, fileHeader, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
|
||||
// Create the uploads folder if it doesn't already exist
|
||||
err = os.MkdirAll("./images", os.ModePerm)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Create a new file in the uploads directory
|
||||
filename := fmt.Sprintf("/images/%d%s", time.Now().UnixNano(), filepath.Ext(fileHeader.Filename))
|
||||
dst, err := os.Create("." + filename)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
defer dst.Close()
|
||||
|
||||
// Copy the uploaded file to the specified destination
|
||||
_, err = io.Copy(dst, file)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
fmt.Println(filename)
|
||||
response, _ := json.Marshal(map[string]string{"location": filename})
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
w.Write(response)
|
||||
}
|
||||
|
||||
func ServeImages(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Println(r.URL)
|
||||
fs := http.StripPrefix("/images/", http.FileServer(http.Dir("./images")))
|
||||
fs.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func ChangeMethod(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodPost {
|
||||
switch method := r.PostFormValue("_method"); method {
|
||||
case http.MethodPut:
|
||||
fallthrough
|
||||
case http.MethodPatch:
|
||||
fallthrough
|
||||
case http.MethodDelete:
|
||||
r.Method = method
|
||||
default:
|
||||
}
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func ArticleCtx(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
articleID := chi.URLParam(r, "articleID")
|
||||
article, err := dbGetArticle(articleID)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
http.Error(w, http.StatusText(404), 404)
|
||||
return
|
||||
}
|
||||
ctx := context.WithValue(r.Context(), "article", article)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
func GetAllArticles(w http.ResponseWriter, r *http.Request) {
|
||||
articles, err := dbGetAllArticles()
|
||||
catch(err)
|
||||
|
||||
t, _ := template.ParseFiles("templates/base.html", "templates/index.html")
|
||||
err = t.Execute(w, articles)
|
||||
catch(err)
|
||||
}
|
||||
|
||||
func NewArticle(w http.ResponseWriter, r *http.Request) {
|
||||
t, _ := template.ParseFiles("templates/base.html", "templates/new.html")
|
||||
err := t.Execute(w, nil)
|
||||
catch(err)
|
||||
}
|
||||
|
||||
func CreateArticle(w http.ResponseWriter, r *http.Request) {
|
||||
title := r.FormValue("title")
|
||||
content := r.FormValue("content")
|
||||
article := &Article{
|
||||
Title: title,
|
||||
Content: template.HTML(content),
|
||||
}
|
||||
|
||||
err := dbCreateArticle(article)
|
||||
catch(err)
|
||||
http.Redirect(w, r, "/", http.StatusFound)
|
||||
}
|
||||
|
||||
func GetArticle(w http.ResponseWriter, r *http.Request) {
|
||||
article := r.Context().Value("article").(*Article)
|
||||
t, _ := template.ParseFiles("templates/base.html", "templates/article.html")
|
||||
err := t.Execute(w, article)
|
||||
catch(err)
|
||||
}
|
||||
|
||||
func EditArticle(w http.ResponseWriter, r *http.Request) {
|
||||
article := r.Context().Value("article").(*Article)
|
||||
|
||||
t, _ := template.ParseFiles("templates/base.html", "templates/edit.html")
|
||||
err := t.Execute(w, article)
|
||||
catch(err)
|
||||
}
|
||||
|
||||
func UpdateArticle(w http.ResponseWriter, r *http.Request) {
|
||||
article := r.Context().Value("article").(*Article)
|
||||
|
||||
title := r.FormValue("title")
|
||||
content := r.FormValue("content")
|
||||
newArticle := &Article{
|
||||
Title: title,
|
||||
Content: template.HTML(content),
|
||||
}
|
||||
fmt.Println(newArticle.Content)
|
||||
err := dbUpdateArticle(strconv.Itoa(article.ID), newArticle)
|
||||
catch(err)
|
||||
http.Redirect(w, r, fmt.Sprintf("/articles/%d", article.ID), http.StatusFound)
|
||||
}
|
||||
|
||||
func DeleteArticle(w http.ResponseWriter, r *http.Request) {
|
||||
article := r.Context().Value("article").(*Article)
|
||||
err := dbDeleteArticle(strconv.Itoa(article.ID))
|
||||
catch(err)
|
||||
|
||||
http.Redirect(w, r, "/", http.StatusFound)
|
||||
}
|
||||
15
go-code-samples/GoBlog/templates/article.html
Normal file
15
go-code-samples/GoBlog/templates/article.html
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{{define "title"}}{{.Title}}{{end}}
|
||||
{{define "scripts"}}{{end}}
|
||||
{{define "body"}}
|
||||
<h1>{{.Title}} </h1>
|
||||
<div>
|
||||
{{.Content}}
|
||||
</div>
|
||||
<div>
|
||||
<a href="/articles/{{.ID}}/edit">Edit</a>
|
||||
<form action="/articles/{{.ID}}" method="post">
|
||||
<input type="hidden" name="_method" value="DELETE">
|
||||
<button type="submit">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
{{end}}
|
||||
11
go-code-samples/GoBlog/templates/base.html
Normal file
11
go-code-samples/GoBlog/templates/base.html
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{{ template "title" . }}</title>
|
||||
{{ template "scripts" }}
|
||||
</head>
|
||||
<body>
|
||||
{{ template "body" . }}
|
||||
</body>
|
||||
</html>
|
||||
26
go-code-samples/GoBlog/templates/edit.html
Normal file
26
go-code-samples/GoBlog/templates/edit.html
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{{define "title"}}Create new article{{end}}
|
||||
{{define "scripts"}}
|
||||
<script src="https://cdn.tiny.cloud/1/no-api-key/tinymce/6/tinymce.min.js" referrerpolicy="origin"></script>
|
||||
<script>
|
||||
tinymce.init({
|
||||
selector: '#mytextarea',
|
||||
plugins: 'image',
|
||||
toolbar: 'undo redo | blocks | image | ' +
|
||||
'bold italic backcolor | alignleft aligncenter ' +
|
||||
'alignright alignjustify | bullist numlist outdent indent | ' +
|
||||
'removeformat | help',
|
||||
images_upload_url: "/upload",
|
||||
relative_urls : false,
|
||||
remove_script_host : false,
|
||||
convert_urls : true,
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
||||
{{define "body"}}
|
||||
<form method="post" action="/articles/{{.ID}}">
|
||||
<input type="text" name="title" value="{{.Title}}">
|
||||
<textarea id="mytextarea" name="content">{{.Content}}</textarea>
|
||||
<input type="hidden" name="_method" value="PUT">
|
||||
<button id="submit" type="submit" onclick="submitForm()">Edit</button>
|
||||
</form>
|
||||
{{end}}
|
||||
16
go-code-samples/GoBlog/templates/index.html
Normal file
16
go-code-samples/GoBlog/templates/index.html
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{{define "title"}}All articles{{end}}
|
||||
{{define "scripts"}}{{end}}
|
||||
{{define "body"}}
|
||||
{{if eq (len .) 0}}
|
||||
Nothing to see here
|
||||
{{end}}
|
||||
{{range .}}
|
||||
<div>
|
||||
<a href="/articles/{{.ID}}">{{.Title}}</a>
|
||||
</div>
|
||||
{{end}}
|
||||
<p>
|
||||
<a href="/articles">Create new article</a>
|
||||
|
||||
</p>
|
||||
{{end}}
|
||||
25
go-code-samples/GoBlog/templates/new.html
Normal file
25
go-code-samples/GoBlog/templates/new.html
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{{define "title"}}Create new article{{end}}
|
||||
{{define "scripts"}}
|
||||
<script src="https://cdn.tiny.cloud/1/no-api-key/tinymce/6/tinymce.min.js" referrerpolicy="origin"></script>
|
||||
<script>
|
||||
tinymce.init({
|
||||
selector: '#mytextarea',
|
||||
plugins: 'image',
|
||||
toolbar: 'undo redo | blocks | image | ' +
|
||||
'bold italic backcolor | alignleft aligncenter ' +
|
||||
'alignright alignjustify | bullist numlist outdent indent | ' +
|
||||
'removeformat | help',
|
||||
images_upload_url: "/upload",
|
||||
relative_urls : false,
|
||||
remove_script_host : false,
|
||||
convert_urls : true,
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
||||
{{define "body"}}
|
||||
<form method="post" action="/articles">
|
||||
<input type="text" name="title" placeholder="Enter the title">
|
||||
<textarea id="mytextarea" name="content"></textarea>
|
||||
<button id="submit" type="submit">Create</button>
|
||||
</form>
|
||||
{{end}}
|
||||
201
go-code-samples/LICENSE
Normal file
201
go-code-samples/LICENSE
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
5
go-code-samples/README.md
Normal file
5
go-code-samples/README.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
[](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub)
|
||||
|
||||
# go-code-samples
|
||||
|
||||
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.
|
||||
3
go-code-samples/awesomeProject/error-handling/go.mod
Normal file
3
go-code-samples/awesomeProject/error-handling/go.mod
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
module error-handling
|
||||
|
||||
go 1.20
|
||||
91
go-code-samples/awesomeProject/error-handling/main.go
Normal file
91
go-code-samples/awesomeProject/error-handling/main.go
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/fs"
|
||||
"log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.SetFlags(0) // no time stamp
|
||||
|
||||
// Read a single file
|
||||
log.SetPrefix("Reading a single file: ")
|
||||
|
||||
_, err := ReadFile("no/file")
|
||||
if err != nil {
|
||||
log.Println("err = ", err)
|
||||
}
|
||||
|
||||
// Unwrap the error returned by os.Open()
|
||||
log.Println("errors.Unwrap(err) = ", errors.Unwrap(err))
|
||||
|
||||
// Confirm that the error is, or wraps, an fs.ErrNotExist error
|
||||
log.Println("err is fs.ErrNotExist:", errors.Is(err, fs.ErrNotExist))
|
||||
|
||||
// Confirm that the error is, or wraps, an fs.PathError.
|
||||
// errors.As() assigns the unwrapped PathError to target.
|
||||
// This allows reading PathError's Path field.
|
||||
target := &fs.PathError{}
|
||||
if errors.As(err, &target) {
|
||||
log.Printf("err as PathError: path is '%s'\n", target.Path)
|
||||
log.Printf("err as PathError: op is '%s'\n", target.Op)
|
||||
}
|
||||
|
||||
// Read files concurrently - handling context errors
|
||||
log.SetPrefix("Reading files concurrently: ")
|
||||
|
||||
_, err = ReadFilesConcurrently([]string{"no/file/a", "no/file/b", "no/file/c"})
|
||||
log.Println("err = ", err)
|
||||
|
||||
// Read multiple files
|
||||
log.SetPrefix("Reading multiple files: ")
|
||||
|
||||
// Passing an empty slice triggers a plain error
|
||||
_, err = ReadFiles([]string{})
|
||||
log.Println("err = ", err)
|
||||
|
||||
// Passing multiple paths of non-existing files triggers a joined error
|
||||
_, err = ReadFiles([]string{"no/file/a", "no/file/b", "no/file/c"})
|
||||
log.Println("joined errors = ", err)
|
||||
|
||||
// Unwrap the errors inside the joined error
|
||||
// A joined error does not have the method "func Unwrap() error"
|
||||
// because it does not wrap a single error but rather a slice of errors.
|
||||
// Therefore, errors.Unwrap() cannot unwrap the errors and returns nil.
|
||||
log.Println("errors.Unwrap(err) = ", errors.Unwrap(err))
|
||||
|
||||
// To unwrap a joined error, you can type-assert that err has
|
||||
// an Unwrap() method that returns a slice of errors.
|
||||
e, ok := err.(interface{ Unwrap() []error })
|
||||
if ok {
|
||||
log.Println("e.Unwrap() = ", e.Unwrap())
|
||||
}
|
||||
|
||||
// Network errors
|
||||
log.SetPrefix("Network errors: ")
|
||||
|
||||
err = connectToTCPServer()
|
||||
log.Println("err = ", err)
|
||||
|
||||
// Recover from a panic
|
||||
log.SetPrefix("Recovering from a panic: ")
|
||||
|
||||
// This example is at the end of main, because the panic
|
||||
// causes main to exit. Only the deferred function is
|
||||
// called before exiting.
|
||||
|
||||
defer func() {
|
||||
// Is this func invoked from a panic?
|
||||
if r := recover(); r != nil {
|
||||
// Yes: recover from the panic
|
||||
log.Printf("isValidPath panicked: error is '%v'\n", r)
|
||||
// ...
|
||||
}
|
||||
}()
|
||||
|
||||
// isValidPath panics because of an invalid regexp.
|
||||
if isValidPath("/path/to/file") {
|
||||
_, _ = ReadFile("/path/to/file")
|
||||
}
|
||||
}
|
||||
36
go-code-samples/awesomeProject/error-handling/network.go
Normal file
36
go-code-samples/awesomeProject/error-handling/network.go
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
)
|
||||
|
||||
// Connect to a TCP server and check the error. use errors.As() to unwrap the net.OpError, and test if the error is transient.
|
||||
func connectToTCPServer() error {
|
||||
var err error
|
||||
var conn net.Conn
|
||||
for retry := 3; retry > 0; retry-- {
|
||||
conn, err = net.Dial("tcp", "127.0.0.1:12345")
|
||||
if err != nil {
|
||||
// Check if err is a net.OpError
|
||||
opErr := &net.OpError{}
|
||||
if errors.As(err, &opErr) {
|
||||
log.Println("err is net.OpError:", opErr.Error())
|
||||
// test if the error is temporary
|
||||
if opErr.Temporary() {
|
||||
log.Printf("Retrying...\n")
|
||||
continue
|
||||
}
|
||||
retry = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("connect failed: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
// send or receive data
|
||||
return nil
|
||||
}
|
||||
30
go-code-samples/awesomeProject/error-handling/readfile.go
Normal file
30
go-code-samples/awesomeProject/error-handling/readfile.go
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
func ReadFile(path string) ([]byte, error) {
|
||||
if path == "" {
|
||||
// Create an error with errors.New()
|
||||
return nil, errors.New("path is empty")
|
||||
}
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
// Wrap the error.
|
||||
// If the format string uses %w to format the error,
|
||||
// fmt.Errorf() returns an error that has the
|
||||
// method "func Unwrap() error" implemented.
|
||||
return nil, fmt.Errorf("open failed: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
buf, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read failed: %w", err)
|
||||
}
|
||||
return buf, nil
|
||||
}
|
||||
33
go-code-samples/awesomeProject/error-handling/readfiles.go
Normal file
33
go-code-samples/awesomeProject/error-handling/readfiles.go
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func ReadFiles(paths []string) ([][]byte, error) {
|
||||
var errs error
|
||||
var contents [][]byte
|
||||
|
||||
if len(paths) == 0 {
|
||||
// Create a new error with fmt.Errorf() (but without using %w):
|
||||
return nil, fmt.Errorf("no paths provided: paths slice is %v", paths)
|
||||
}
|
||||
|
||||
for _, path := range paths {
|
||||
content, err := ReadFile(path)
|
||||
if err != nil {
|
||||
// Join all errors that occur into errs.
|
||||
// The returned error type implements method "func Unwrap() []error".
|
||||
// (Note that the return type is a slice.)
|
||||
errs = errors.Join(errs, fmt.Errorf("reading %s failed: %w", path, err))
|
||||
continue
|
||||
}
|
||||
contents = append(contents, content)
|
||||
}
|
||||
|
||||
// Some files may have been read, some may have failed to be read.
|
||||
// Therefore, ReadFiles returns both return values, regardless
|
||||
// of whether there have been errors.
|
||||
return contents, errs
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Goal: Read multiple files concurrently. All files must be read successfully.
|
||||
// If one goroutine fails, cancel all the other goroutines.
|
||||
//
|
||||
// The cancelled goroutines can inspect the error from canceling the context with ctx.Err()..
|
||||
|
||||
func ReadFilesConcurrently(paths []string) ([][]byte, error) {
|
||||
var contents [][]byte
|
||||
|
||||
ctx, cancel := context.WithCancelCause(context.Background())
|
||||
defer cancel(nil)
|
||||
|
||||
resCh := make(chan []byte)
|
||||
wg := sync.WaitGroup{}
|
||||
|
||||
for _, path := range paths {
|
||||
wg.Add(1)
|
||||
go func(ctx context.Context, cancel context.CancelCauseFunc, p string, resCh chan<- []byte, wg *sync.WaitGroup) {
|
||||
time.Sleep(time.Duration(rand.Intn(10)) * time.Microsecond) // simulate workload
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Printf("ReadFilesConcurrently (goroutine): Context canceled for path %s: %v", p, ctx.Err())
|
||||
wg.Done()
|
||||
return
|
||||
default:
|
||||
// If an error occurs here, cancel all the other goroutines
|
||||
content, err := ReadFile(p)
|
||||
if err != nil {
|
||||
cancel(fmt.Errorf("error reading %s: %w", p, err))
|
||||
log.Printf("ReadFilesConcurrently (goroutine): Context canceled for path %s: %v", p, err)
|
||||
wg.Done()
|
||||
return
|
||||
}
|
||||
resCh <- content
|
||||
}
|
||||
}(ctx, cancel, path, resCh, &wg)
|
||||
}
|
||||
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(resCh)
|
||||
}()
|
||||
|
||||
for c := range resCh {
|
||||
contents = append(contents, c)
|
||||
}
|
||||
|
||||
if e := ctx.Err(); e != nil {
|
||||
return nil, fmt.Errorf("ReadFilesConcurrently: %w", e)
|
||||
}
|
||||
return contents, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package main
|
||||
|
||||
import "regexp"
|
||||
|
||||
func isValidPath(p string) bool {
|
||||
pathRe := regexp.MustCompile(`(invalid regular expression`)
|
||||
return pathRe.MatchString(p)
|
||||
}
|
||||
5
go-code-samples/database-sql-package-goproject/README.md
Normal file
5
go-code-samples/database-sql-package-goproject/README.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# Getting Started with The database/sql Package
|
||||
|
||||
This hands-on tutorial will show you how to get started with the database/sql package.
|
||||
|
||||
Find the tutorial [here](https://blog.jetbrains.com/go/2023/02/28/getting-started-with-the-database-sql-package/).
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
var db *sql.DB
|
||||
|
||||
type Album struct {
|
||||
ID int64
|
||||
Title string
|
||||
Artist string
|
||||
Price float32
|
||||
Quantity int64
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Capture connection properties.
|
||||
cfg := mysql.Config{
|
||||
User: os.Getenv("DBUSER"),
|
||||
Passwd: os.Getenv("DBPASS"),
|
||||
Net: "tcp",
|
||||
Addr: "127.0.0.1:3306",
|
||||
DBName: "recordings",
|
||||
}
|
||||
// Get a database handle.
|
||||
var err error
|
||||
db, err = sql.Open("mysql", cfg.FormatDSN())
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
pingErr := db.Ping()
|
||||
if pingErr != nil {
|
||||
log.Fatal(pingErr)
|
||||
}
|
||||
fmt.Println("Connected!")
|
||||
}
|
||||
5
go-code-samples/database-sql-package-goproject/go.mod
Normal file
5
go-code-samples/database-sql-package-goproject/go.mod
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
module database-sql-package-goproject
|
||||
|
||||
go 1.19
|
||||
|
||||
require github.com/go-sql-driver/mysql v1.7.0 // indirect
|
||||
2
go-code-samples/database-sql-package-goproject/go.sum
Normal file
2
go-code-samples/database-sql-package-goproject/go.sum
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
|
||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
var db *sql.DB
|
||||
|
||||
type Album struct {
|
||||
ID int64
|
||||
Title string
|
||||
Artist string
|
||||
Price float32
|
||||
Quantity int64
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Capture connection properties.
|
||||
cfg := mysql.Config{
|
||||
User: os.Getenv("DBUSER"),
|
||||
Passwd: os.Getenv("DBPASS"),
|
||||
Net: "tcp",
|
||||
Addr: "127.0.0.1:3306",
|
||||
DBName: "recordings",
|
||||
}
|
||||
// Get a database handle.
|
||||
var err error
|
||||
db, err = sql.Open("mysql", cfg.FormatDSN())
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
pingErr := db.Ping()
|
||||
if pingErr != nil {
|
||||
log.Fatal(pingErr)
|
||||
}
|
||||
fmt.Println("Connected!")
|
||||
|
||||
albID, err := addAlbum(Album{
|
||||
Title: "The Modern Sound of Betty Carter",
|
||||
Artist: "Betty Carter",
|
||||
Price: 49.99,
|
||||
Quantity: 10,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("ID of added album: %v\n", albID)
|
||||
}
|
||||
|
||||
// addAlbum adds the specified album to the database,
|
||||
// returning the album ID of the new entry
|
||||
func addAlbum(alb Album) (int64, error) {
|
||||
result, err := db.Exec("INSERT INTO album (title, artist, price, quantity) VALUES (?, ?, ?, ?)", alb.Title, alb.Artist, alb.Price, alb.Quantity)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("addAlbum: %v", err)
|
||||
}
|
||||
id, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("addAlbum: %v", err)
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
var db *sql.DB
|
||||
|
||||
type Album struct {
|
||||
ID int64
|
||||
Title string
|
||||
Artist string
|
||||
Price float32
|
||||
Quantity int64
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Capture connection properties.
|
||||
cfg := mysql.Config{
|
||||
User: os.Getenv("DBUSER"),
|
||||
Passwd: os.Getenv("DBPASS"),
|
||||
Net: "tcp",
|
||||
Addr: "127.0.0.1:3306",
|
||||
DBName: "recordings",
|
||||
}
|
||||
// Get a database handle.
|
||||
var err error
|
||||
db, err = sql.Open("mysql", cfg.FormatDSN())
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
pingErr := db.Ping()
|
||||
if pingErr != nil {
|
||||
log.Fatal(pingErr)
|
||||
}
|
||||
fmt.Println("Connected!")
|
||||
|
||||
albums, err := albumsByArtist("John Coltrane")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("Albums found: %v\n", albums)
|
||||
}
|
||||
|
||||
// albumsByArtist queries for albums that have the specified artist name.
|
||||
func albumsByArtist(name string) ([]Album, error) {
|
||||
// An albums slice to hold data from returned rows.
|
||||
var albums []Album
|
||||
|
||||
rows, err := db.Query("SELECT * FROM album WHERE artist = ?", name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("albumsByArtist %q: %v", name, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
// Loop through rows, using Scan to assign column data to struct fields.
|
||||
for rows.Next() {
|
||||
var alb Album
|
||||
if err := rows.Scan(&alb.ID, &alb.Title, &alb.Artist, &alb.Price, &alb.Quantity); err != nil {
|
||||
return nil, fmt.Errorf("albumsByArtist %q: %v", name, err)
|
||||
}
|
||||
albums = append(albums, alb)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("albumsByArtist %q: %v", name, err)
|
||||
}
|
||||
return albums, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
var db *sql.DB
|
||||
|
||||
type Album struct {
|
||||
ID int64
|
||||
Title string
|
||||
Artist string
|
||||
Price float32
|
||||
Quantity int64
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Capture connection properties.
|
||||
cfg := mysql.Config{
|
||||
User: os.Getenv("DBUSER"),
|
||||
Passwd: os.Getenv("DBPASS"),
|
||||
Net: "tcp",
|
||||
Addr: "127.0.0.1:3306",
|
||||
DBName: "recordings",
|
||||
}
|
||||
// Get a database handle.
|
||||
var err error
|
||||
db, err = sql.Open("mysql", cfg.FormatDSN())
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
pingErr := db.Ping()
|
||||
if pingErr != nil {
|
||||
log.Fatal(pingErr)
|
||||
}
|
||||
fmt.Println("Connected!")
|
||||
|
||||
// Hard-code ID 2 here to test the query.
|
||||
Album, err := albumByID(2)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("Album found: %v\n", Album)
|
||||
}
|
||||
|
||||
// AlbumByID retrieves the specified album.
|
||||
func albumByID(id int) (Album, error) {
|
||||
// Define a prepared statement. You'd typically define the statement
|
||||
// elsewhere and save it for use in functions such as this one.
|
||||
stmt, err := db.Prepare("SELECT * FROM album WHERE id = ?")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var album Album
|
||||
|
||||
// Execute the prepared statement, passing in an id value for the
|
||||
// parameter whose placeholder is ?
|
||||
err = stmt.QueryRow(id).Scan(&album.ID, &album.Title, &album.Artist, &album.Price, &album.Quantity)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
// Handle the case of no rows returned.
|
||||
}
|
||||
return album, err
|
||||
}
|
||||
return album, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
var db *sql.DB
|
||||
|
||||
type Album struct {
|
||||
ID int64
|
||||
Title string
|
||||
Artist string
|
||||
Price float32
|
||||
Quantity int64
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Capture connection properties.
|
||||
cfg := mysql.Config{
|
||||
User: os.Getenv("DBUSER"),
|
||||
Passwd: os.Getenv("DBPASS"),
|
||||
Net: "tcp",
|
||||
Addr: "127.0.0.1:3306",
|
||||
DBName: "recordings",
|
||||
}
|
||||
// Get a database handle.
|
||||
var err error
|
||||
db, err = sql.Open("mysql", cfg.FormatDSN())
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
pingErr := db.Ping()
|
||||
if pingErr != nil {
|
||||
log.Fatal(pingErr)
|
||||
}
|
||||
fmt.Println("Connected!")
|
||||
|
||||
// Hard-code ID 2 here to test the query.
|
||||
alb, err := albumByID(2)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("Album found: %v\n", alb)
|
||||
}
|
||||
|
||||
// albumByID queries for the album with the specified ID.
|
||||
func albumByID(id int64) (Album, error) {
|
||||
// An album to hold data from the returned row.
|
||||
var alb Album
|
||||
|
||||
row := db.QueryRow("SELECT * FROM album WHERE id = ?", id)
|
||||
if err := row.Scan(&alb.ID, &alb.Title, &alb.Artist, &alb.Price, &alb.Quantity); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return alb, fmt.Errorf("albumsById %d: no such album", id)
|
||||
}
|
||||
return alb, fmt.Errorf("albumsById %d: %v", id, err)
|
||||
}
|
||||
return alb, nil
|
||||
}
|
||||
17
go-code-samples/database-sql-package-goproject/table-01.sql
Normal file
17
go-code-samples/database-sql-package-goproject/table-01.sql
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
DROP TABLE IF EXISTS album;
|
||||
CREATE TABLE album (
|
||||
id INT AUTO_INCREMENT NOT NULL,
|
||||
title VARCHAR(128) NOT NULL,
|
||||
artist VARCHAR(255) NOT NULL,
|
||||
price DECIMAL(5,2) NOT NULL,
|
||||
quantity INT UNSIGNED,
|
||||
PRIMARY KEY (`id`)
|
||||
);
|
||||
|
||||
INSERT INTO album
|
||||
(title, artist, price, quantity)
|
||||
VALUES
|
||||
('Blue Train', 'John Coltrane', 56.99, 5),
|
||||
('Giant Steps', 'John Coltrane', 63.99, 62),
|
||||
('Jeru', 'Gerry Mulligan', 17.99, 0),
|
||||
('Sarah Vaughan', 'Sarah Vaughan', 34.98, 127);
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
DROP TABLE IF EXISTS album_trx;
|
||||
CREATE TABLE album_trx (
|
||||
trx_id INT AUTO_INCREMENT NOT NULL,
|
||||
trx_check INT UNSIGNED,
|
||||
PRIMARY KEY (`trx_id`)
|
||||
);
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
var db *sql.DB
|
||||
|
||||
type Album struct {
|
||||
ID int64
|
||||
Title string
|
||||
Artist string
|
||||
Price float32
|
||||
Quantity int64
|
||||
}
|
||||
|
||||
type Album_trx struct {
|
||||
TRX_ID int64
|
||||
TRX_CHECK int64
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Capture connection properties.
|
||||
cfg := mysql.Config{
|
||||
User: os.Getenv("DBUSER"),
|
||||
Passwd: os.Getenv("DBPASS"),
|
||||
Net: "tcp",
|
||||
Addr: "127.0.0.1:3306",
|
||||
DBName: "recordings",
|
||||
}
|
||||
// Get a database handle.
|
||||
var err error
|
||||
db, err = sql.Open("mysql", cfg.FormatDSN())
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
pingErr := db.Ping()
|
||||
if pingErr != nil {
|
||||
log.Fatal(pingErr)
|
||||
}
|
||||
fmt.Println("Connected!")
|
||||
|
||||
// Start the transaction
|
||||
ctx := context.Background()
|
||||
tx, err := db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// First query
|
||||
_, err = tx.ExecContext(ctx, "INSERT INTO album (title, artist, price, quantity) VALUES ('Master of Puppets', 'Metallica', '49', '1')")
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return
|
||||
}
|
||||
|
||||
// Second query
|
||||
_, err = tx.ExecContext(ctx, "INSERT INTO album_trx (trx_check) VALUES (1)")
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
fmt.Println("Transaction declined")
|
||||
return
|
||||
}
|
||||
|
||||
// If no errors, commit the transaction
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println("Transaction accepted!")
|
||||
}
|
||||
8
go-code-samples/get-started-with-redis/.idea/.gitignore
vendored
Normal file
8
go-code-samples/get-started-with-redis/.idea/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="RedisDemo 1: ping" type="GoApplicationRunConfiguration" factoryName="Go Application">
|
||||
<module name="get-started-with-redis" />
|
||||
<working_directory value="$PROJECT_DIR$" />
|
||||
<parameters value="ping" />
|
||||
<kind value="PACKAGE" />
|
||||
<package value="github.com/JetBrains/go-code-samples/get-started-with-redis" />
|
||||
<directory value="$PROJECT_DIR$" />
|
||||
<filePath value="$PROJECT_DIR$" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="RedisDemo 2: get/set" type="GoApplicationRunConfiguration" factoryName="Go Application">
|
||||
<module name="get-started-with-redis" />
|
||||
<working_directory value="$PROJECT_DIR$" />
|
||||
<parameters value="getset" />
|
||||
<kind value="PACKAGE" />
|
||||
<package value="github.com/JetBrains/go-code-samples/get-started-with-redis" />
|
||||
<directory value="$PROJECT_DIR$" />
|
||||
<filePath value="$PROJECT_DIR$" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="RedisDemo 3: expire" type="GoApplicationRunConfiguration" factoryName="Go Application">
|
||||
<module name="get-started-with-redis" />
|
||||
<working_directory value="$PROJECT_DIR$" />
|
||||
<parameters value="expire" />
|
||||
<kind value="PACKAGE" />
|
||||
<package value="github.com/JetBrains/go-code-samples/get-started-with-redis" />
|
||||
<directory value="$PROJECT_DIR$" />
|
||||
<filePath value="$PROJECT_DIR$" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="RedisDemo 4: pipeline" type="GoApplicationRunConfiguration" factoryName="Go Application">
|
||||
<module name="get-started-with-redis" />
|
||||
<working_directory value="$PROJECT_DIR$" />
|
||||
<parameters value="pipeline" />
|
||||
<kind value="PACKAGE" />
|
||||
<package value="github.com/JetBrains/go-code-samples/get-started-with-redis" />
|
||||
<directory value="$PROJECT_DIR$" />
|
||||
<filePath value="$PROJECT_DIR$" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="RedisDemo 5: transaction" type="GoApplicationRunConfiguration" factoryName="Go Application">
|
||||
<module name="get-started-with-redis" />
|
||||
<working_directory value="$PROJECT_DIR$" />
|
||||
<parameters value="transaction" />
|
||||
<kind value="PACKAGE" />
|
||||
<package value="github.com/JetBrains/go-code-samples/get-started-with-redis" />
|
||||
<directory value="$PROJECT_DIR$" />
|
||||
<filePath value="$PROJECT_DIR$" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="RedisDemo 6: pub/sub" type="GoApplicationRunConfiguration" factoryName="Go Application">
|
||||
<module name="get-started-with-redis" />
|
||||
<working_directory value="$PROJECT_DIR$" />
|
||||
<parameters value="pubsub" />
|
||||
<kind value="PACKAGE" />
|
||||
<package value="github.com/JetBrains/go-code-samples/get-started-with-redis" />
|
||||
<directory value="$PROJECT_DIR$" />
|
||||
<filePath value="$PROJECT_DIR$" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="RedisDemo: pipeline benchmark" type="GoTestRunConfiguration" factoryName="Go Test">
|
||||
<module name="get-started-with-redis" />
|
||||
<working_directory value="$PROJECT_DIR$" />
|
||||
<kind value="DIRECTORY" />
|
||||
<package value="github.com/JetBrains/go-code-samples/get-started-with-redis" />
|
||||
<directory value="$PROJECT_DIR$" />
|
||||
<filePath value="$PROJECT_DIR$" />
|
||||
<framework value="gobench" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="RedisDemo: reset data" type="GoApplicationRunConfiguration" factoryName="Go Application">
|
||||
<module name="get-started-with-redis" />
|
||||
<working_directory value="$PROJECT_DIR$" />
|
||||
<parameters value="reset" />
|
||||
<kind value="PACKAGE" />
|
||||
<package value="github.com/JetBrains/go-code-samples/get-started-with-redis" />
|
||||
<directory value="$PROJECT_DIR$" />
|
||||
<filePath value="$PROJECT_DIR$" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
5
go-code-samples/get-started-with-redis/README.md
Normal file
5
go-code-samples/get-started-with-redis/README.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# Getting Started with Redis in Go
|
||||
|
||||
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.
|
||||
|
||||
Find the tutorial [here]().
|
||||
35
go-code-samples/get-started-with-redis/expiringkeys.go
Normal file
35
go-code-samples/get-started-with-redis/expiringkeys.go
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"time"
|
||||
)
|
||||
|
||||
func expiringKeys(client *redis.Client) error {
|
||||
ctx := context.Background()
|
||||
|
||||
// Add a temporary player
|
||||
err := client.HSet(ctx, "player:10", "name", "Crymyios", "score", 0, "team", "Knucklewimp", "challenges_completed", 0).Err()
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot set player:10: %w", err)
|
||||
}
|
||||
|
||||
// Set an expiration time for player:10
|
||||
if !client.Expire(ctx, "player:10", time.Second).Val() {
|
||||
return fmt.Errorf("cannot set expiration time for player:10")
|
||||
}
|
||||
|
||||
// Get player:10
|
||||
for i := 0; i < 3; i++ {
|
||||
val, err := client.HGet(ctx, "player:10", "name").Result()
|
||||
if err != nil {
|
||||
fmt.Printf("player:10 has expired: %v\n", err)
|
||||
return nil
|
||||
}
|
||||
fmt.Printf("player:10's name: %s\n", val)
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
32
go-code-samples/get-started-with-redis/getandset.go
Normal file
32
go-code-samples/get-started-with-redis/getandset.go
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
// Task: The quest is a lowercase string. Change it to title case.
|
||||
// Then print the challenges in the order they have to be completed.
|
||||
|
||||
func getAndSet(client *redis.Client) error {
|
||||
ctx := context.Background()
|
||||
|
||||
quest, err := client.Get(ctx, "quest").Result()
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot get quest: %w", err)
|
||||
}
|
||||
|
||||
quest = cases.Title(language.English).String(quest)
|
||||
|
||||
err = client.Set(ctx, "quest", quest, 0).Err()
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot update quest: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Quest is now: %s\n", client.Get(ctx, "quest").Val())
|
||||
|
||||
return nil
|
||||
}
|
||||
13
go-code-samples/get-started-with-redis/go.mod
Normal file
13
go-code-samples/get-started-with-redis/go.mod
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
module github.com/JetBrains/go-code-samples/get-started-with-redis
|
||||
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/redis/go-redis/v9 v9.0.5
|
||||
golang.org/x/text v0.9.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
)
|
||||
18
go-code-samples/get-started-with-redis/go.sum
Normal file
18
go-code-samples/get-started-with-redis/go.sum
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao=
|
||||
github.com/bsm/ginkgo/v2 v2.7.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w=
|
||||
github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y=
|
||||
github.com/bsm/gomega v1.26.0/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/redis/go-redis/v9 v9.0.5 h1:CuQcn5HIEeK7BgElubPP8CGtE0KakrnbBSTLjathl5o=
|
||||
github.com/redis/go-redis/v9 v9.0.5/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk=
|
||||
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
103
go-code-samples/get-started-with-redis/main.go
Normal file
103
go-code-samples/get-started-with-redis/main.go
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
const (
|
||||
dbconn = "localhost:6379"
|
||||
db = 0
|
||||
)
|
||||
|
||||
func usage() {
|
||||
fmt.Println(`Usage: go run main.go <demo>
|
||||
Where <demo> is one of:
|
||||
- ping: run the ping command
|
||||
- getset: get and set a string value
|
||||
- expire: set an expiring key
|
||||
- pipeline: run a batch of commands
|
||||
- transaction: run a batch of commands that must all succeed
|
||||
- pubsub: send a message to a channel and listen to the channel
|
||||
- reset: restore the initial data set`)
|
||||
}
|
||||
|
||||
func run() error {
|
||||
// Create a new client from a connection string and a database number (0-15)
|
||||
client := newClient(dbconn, 0)
|
||||
|
||||
if len(os.Args) < 2 {
|
||||
usage()
|
||||
return nil
|
||||
}
|
||||
|
||||
switch os.Args[1] {
|
||||
case "ping":
|
||||
// Ping the redis server and fetch some database information.
|
||||
fmt.Printf("\nPing: Test the connection\n")
|
||||
ping(client)
|
||||
|
||||
case "getset":
|
||||
// Get and set a string value.
|
||||
fmt.Printf("\nGet/Set: Update the quest to title case\n")
|
||||
err := getAndSet(client)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getAndSet failed: %w", err)
|
||||
}
|
||||
|
||||
case "expire":
|
||||
// Set an expiring key and wait for it to expire
|
||||
fmt.Printf("\nExpire: Add a player temporarily\n")
|
||||
err := expiringKeys(client)
|
||||
if err != nil {
|
||||
return fmt.Errorf("expiringKeys failed: %w", err)
|
||||
}
|
||||
|
||||
case "pipeline":
|
||||
// Run a batch of commands.
|
||||
fmt.Printf("\nPipeline: Update score and challenges_completed for team Snarkdumbthimble\n")
|
||||
err := pipeline(client)
|
||||
if err != nil {
|
||||
return fmt.Errorf("pipeline failed: %w", err)
|
||||
}
|
||||
|
||||
case "transaction":
|
||||
// Run several commands that must all succeed.
|
||||
// If any of them fails, the transaction will be canceled.
|
||||
fmt.Printf("\nTransaction: Rearrange the teams\n")
|
||||
err := transaction(client)
|
||||
if err != nil {
|
||||
return fmt.Errorf("transaction failed: %w", err)
|
||||
}
|
||||
|
||||
case "pubsub":
|
||||
// Send messages to a publish/receive channel.
|
||||
// Listen to the channel and receive the messages.
|
||||
fmt.Printf("\nPub/Sub: Publish challenges to subscribed teams\n")
|
||||
err := pubsub(client)
|
||||
if err != nil {
|
||||
return fmt.Errorf("pubsub stopped: %w", err)
|
||||
}
|
||||
|
||||
case "reset":
|
||||
// Reset the database
|
||||
fmt.Printf("\nReset the database\n")
|
||||
err := resetdata(client)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot reset database: %w", err)
|
||||
}
|
||||
default:
|
||||
usage()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
err := run()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
23
go-code-samples/get-started-with-redis/ping.go
Normal file
23
go-code-samples/get-started-with-redis/ping.go
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
func ping(client *redis.Client) error {
|
||||
// For the demo, we need only a background context
|
||||
ctx := context.Background()
|
||||
// Ping the redis server. It should respond with "PONG".
|
||||
fmt.Println(client.Ping(ctx))
|
||||
|
||||
// Get the client info.
|
||||
info, err := client.ClientInfo(ctx).Result()
|
||||
if err != nil {
|
||||
return fmt.Errorf("method ClientInfo failed: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("%#v\n", info)
|
||||
return nil
|
||||
}
|
||||
43
go-code-samples/get-started-with-redis/pipeline.go
Normal file
43
go-code-samples/get-started-with-redis/pipeline.go
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// Task: Update the score and the challenges_completed
|
||||
// for team Snarkdumbthimble that has finished challenge #1.
|
||||
|
||||
func pipeline(client *redis.Client) error {
|
||||
ctx := context.Background()
|
||||
|
||||
_, err := client.Pipelined(ctx, func(pipe redis.Pipeliner) error {
|
||||
err := pipe.HSet(ctx, "player:7", "score", 15, "challenges_completed", 1).Err()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = pipe.HSet(ctx, "player:8", "score", 18, "challenges_completed", 1).Err()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = pipe.HSet(ctx, "player:9", "score", 12, "challenges_completed", 1).Err()
|
||||
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("pipelined failed: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Player 7's score: %s, challenges completed: %s\n",
|
||||
client.HGet(ctx, "player:7", "score").Val(),
|
||||
client.HGet(ctx, "player:7", "challenges_completed").Val())
|
||||
fmt.Printf("Player 8's score: %s, challenges completed: %s\n",
|
||||
client.HGet(ctx, "player:8", "score").Val(),
|
||||
client.HGet(ctx, "player:8", "challenges_completed").Val())
|
||||
fmt.Printf("Player 9's score: %s, challenges completed: %s\n",
|
||||
client.HGet(ctx, "player:9", "score").Val(),
|
||||
client.HGet(ctx, "player:9", "challenges_completed").Val())
|
||||
|
||||
return nil
|
||||
}
|
||||
53
go-code-samples/get-started-with-redis/pipeline_test.go
Normal file
53
go-code-samples/get-started-with-redis/pipeline_test.go
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func incrementScorePipe(client *redis.Client, player string) error {
|
||||
ctx := context.Background()
|
||||
client.Pipelined(ctx, func(pipe redis.Pipeliner) error {
|
||||
for i := 0; i < 1000; i++ {
|
||||
err := pipe.HIncrBy(ctx, player, "score", 1).Err()
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot increment score for player %s to %d: %w", player, i, err)
|
||||
}
|
||||
}
|
||||
pipe.HSet(ctx, player, "score", 1)
|
||||
return nil
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func incrementScoreNoPipe(client *redis.Client, player string) error {
|
||||
ctx := context.Background()
|
||||
for i := 0; i < 1000; i++ {
|
||||
err := client.HIncrBy(ctx, player, "score", 1).Err()
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot increment score for player %s to %d: %w", player, i, err)
|
||||
}
|
||||
}
|
||||
client.HSet(ctx, player, "score", 1)
|
||||
return nil
|
||||
}
|
||||
|
||||
func BenchmarkPipeline(b *testing.B) {
|
||||
client := newClient(dbconn, 0)
|
||||
b.Run("PipelinedHIncrBy", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
incrementScorePipe(client, "player:1")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkNoPipeline(b *testing.B) {
|
||||
client := newClient(dbconn, 0)
|
||||
b.Run("HIncrBy", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
incrementScoreNoPipe(client, "player:2")
|
||||
}
|
||||
})
|
||||
}
|
||||
164
go-code-samples/get-started-with-redis/pubsub.go
Normal file
164
go-code-samples/get-started-with-redis/pubsub.go
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Task: Send each challenge to the "challenge" channel.
|
||||
// Each team is subscribed to the "challenge" channel and enters the challenge upon receiving.
|
||||
|
||||
const (
|
||||
// the name of the PubSub channel
|
||||
pubsubChan = "challenge"
|
||||
)
|
||||
|
||||
// Res is the result of reading the pubsub channel
|
||||
type Res struct {
|
||||
result string
|
||||
err error
|
||||
}
|
||||
|
||||
// Team manages a team's subscription
|
||||
// Each team uses its own client to subscribe to the "challenge" channel
|
||||
type Team struct {
|
||||
name string
|
||||
client *redis.Client
|
||||
channel *redis.PubSub
|
||||
}
|
||||
|
||||
// getTeams scans the database for "team:*" keys
|
||||
// and returns a slice of Team structs with names filled from the keys
|
||||
func getTeams(client *redis.Client) []Team {
|
||||
ctx := context.Background()
|
||||
teams := make([]Team, 3)
|
||||
teamsets := make([]string, 0, 3)
|
||||
keys := make([]string, 0, 3)
|
||||
var cursor uint64
|
||||
for {
|
||||
// Scan returns a slice of matches. The count may or may not be reached
|
||||
// in the first call to Scan, so the code needs to call Scan in a loop and
|
||||
// append the found keys to the teamsets slice until the cursor "returns to 0".
|
||||
var err error
|
||||
keys, cursor, err = client.Scan(ctx, cursor, "team:*", 3).Result()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
teamsets = append(teamsets, keys...)
|
||||
if cursor == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
// Lazily assume that the scan has returned 3 team sets
|
||||
for i := 0; i < 3; i++ {
|
||||
teams[i].name = teamsets[i]
|
||||
// each team uses its own client
|
||||
teams[i].client = newClient(dbconn, 0)
|
||||
}
|
||||
return teams
|
||||
}
|
||||
|
||||
// subscribe subscribes to the "challenge" channel
|
||||
// and waits for the subscription to be completed
|
||||
func (team *Team) subscribe() error {
|
||||
ctx := context.Background()
|
||||
// Subscribe to the "challenge" channel
|
||||
pubSub := team.client.Subscribe(ctx, pubsubChan)
|
||||
|
||||
// The first Subscribe() call creates the channel.
|
||||
// Until that point, any attempt to publish something fails.
|
||||
reply, err := pubSub.Receive(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("subscribing to channel '%s' failed: %w", pubsubChan, err)
|
||||
}
|
||||
// Expected response type is "*Subscription". Otherwise, something failed.
|
||||
switch reply.(type) {
|
||||
case *redis.Subscription:
|
||||
// Success!
|
||||
case *redis.Message:
|
||||
// The channel is already active and contains messages, hence also a success
|
||||
case *redis.Pong:
|
||||
// letL's call it a success
|
||||
default:
|
||||
return fmt.Errorf("subscribing to a channel failed: received a reply of type %T, expected: *redis.Subscription", reply)
|
||||
}
|
||||
|
||||
team.channel = pubSub
|
||||
|
||||
fmt.Printf("%s subscribed to channel '%s'\n", team.name, pubsubChan)
|
||||
return nil
|
||||
}
|
||||
|
||||
// receive receives messages from the "challenge" channel.
|
||||
// It starts a goroutine that reads from the pubsub channel until
|
||||
// the channel is closed or the context is done.
|
||||
func (team *Team) receive(ctx context.Context, resChan chan<- Res) {
|
||||
ch := team.channel.Channel()
|
||||
defer close(resChan)
|
||||
for {
|
||||
select {
|
||||
case msg, ok := <-ch:
|
||||
if !ok {
|
||||
// The pubsub channel has been closed
|
||||
return
|
||||
}
|
||||
resChan <- Res{fmt.Sprintf("%s received challenge '%s'", team.name, msg.Payload), nil}
|
||||
case <-ctx.Done():
|
||||
resChan <- Res{"", ctx.Err()}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// publish publishes the challenge to the "challenge" channel
|
||||
func publish(client *redis.Client, challenge string) error {
|
||||
ctx := context.Background()
|
||||
fmt.Printf("publishing challenge '%s'\n", challenge)
|
||||
return client.Publish(ctx, pubsubChan, challenge).Err()
|
||||
}
|
||||
|
||||
// pubsub subscribes to the "challenge" channel, publishes the challenges,
|
||||
// and receives the published messages.
|
||||
func pubsub(client *redis.Client) (err error) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Step 1: subscribe each team
|
||||
teams := getTeams(client)
|
||||
for i := 0; i < 3; i++ {
|
||||
err = teams[i].subscribe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("subscribing failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: publish challenges
|
||||
// Read the challenges from the sorted set "challenges" and publish them
|
||||
for i := int64(0); i < 5; i++ {
|
||||
challenge := client.ZRange(ctx, "challenges", i, i).Val()[0]
|
||||
err = publish(client, challenge)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot publish challenge %s: %w", challenge, err)
|
||||
}
|
||||
}
|
||||
// Close the channel after one second, to terminate the receive loops.
|
||||
time.AfterFunc(time.Second, func() {
|
||||
teams[0].channel.Close()
|
||||
fmt.Println(`PubSub channel "challenges" closed`)
|
||||
})
|
||||
|
||||
// Step 3: receive published messages
|
||||
rch := make(chan Res)
|
||||
for i := 0; i < 3; i++ {
|
||||
go teams[i].receive(ctx, rch)
|
||||
}
|
||||
for msg := range rch {
|
||||
if msg.err != nil {
|
||||
return fmt.Errorf("cannot receive challenge: %w", msg.err)
|
||||
}
|
||||
fmt.Println(msg.result)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
11
go-code-samples/get-started-with-redis/redisclient.go
Normal file
11
go-code-samples/get-started-with-redis/redisclient.go
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
package main
|
||||
|
||||
import "github.com/redis/go-redis/v9"
|
||||
|
||||
func newClient(conn string, db int) *redis.Client {
|
||||
return redis.NewClient(&redis.Options{
|
||||
Addr: conn,
|
||||
DB: db,
|
||||
Password: "",
|
||||
})
|
||||
}
|
||||
57
go-code-samples/get-started-with-redis/resetdata.go
Normal file
57
go-code-samples/get-started-with-redis/resetdata.go
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
const (
|
||||
setupfile = "testdata/setup.redis"
|
||||
)
|
||||
|
||||
// resetdata flushes all data from the current database (db 0)
|
||||
// and runs the commands from file setup.redis to set up the database.
|
||||
func resetdata(client *redis.Client) error {
|
||||
ctx := context.Background()
|
||||
|
||||
// read file "setup.redis" line by line
|
||||
setup, err := os.Open(setupfile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot open %s: %w", setupfile, err)
|
||||
}
|
||||
defer setup.Close()
|
||||
|
||||
client.FlushDB(ctx) // FlushDB never fails.
|
||||
|
||||
csv := csv.NewReader(setup)
|
||||
csv.Comma = ' '
|
||||
csv.FieldsPerRecord = -1 // Variable number of fields per line
|
||||
|
||||
for {
|
||||
cmd, err := csv.Read()
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("csv: cannot read a line from %s: %w", setupfile, err)
|
||||
}
|
||||
|
||||
// cmd is a slice of strings, Do() expects a slice of 'any'.
|
||||
// The memory layout of the two slice types is not the same,
|
||||
// so we need to convert cmd to a slice of 'any'.
|
||||
doCmd := make([]interface{}, len(cmd))
|
||||
for i, v := range cmd {
|
||||
doCmd[i] = v
|
||||
}
|
||||
|
||||
err = client.Do(ctx, doCmd...).Err()
|
||||
if err != nil {
|
||||
return fmt.Errorf("resetdata: cannot execute '%v': %w", cmd, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
16
go-code-samples/get-started-with-redis/testdata/setup.redis
vendored
Normal file
16
go-code-samples/get-started-with-redis/testdata/setup.redis
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
FLUSHDB
|
||||
SET quest "the chicken coop catastrophe"
|
||||
SET description "Players must rescue a group of chickens that have been taken hostage by a group of evil goblins."
|
||||
HSET "player:1" name Sykios score 0 team Dorkfoot challenges_completed 0
|
||||
HSET "player:2" name Nidios score 0 team Dorkfoot challenges_completed 0
|
||||
HSET "player:3" name Tiaitia score 0 team Dorkfoot challenges_completed 0
|
||||
HSET "player:4" name Belaeos score 0 team Knucklewimp challenges_completed 0
|
||||
HSET "player:5" name Polytia score 0 team Knucklewimp challenges_completed 0
|
||||
HSET "player:6" name Moritia score 0 team Knucklewimp challenges_completed 0
|
||||
HSET "player:7" name Daryos score 0 team Snarkdumbthimble challenges_completed 0
|
||||
HSET "player:8" name Blalios score 0 team Snarkdumbthimble challenges_completed 0
|
||||
HSET "player:9" name Ighteatia score 0 team Snarkdumbthimble challenges_completed 0
|
||||
SADD "team:Dorkfoot" Sykios Nidios Tiaitia
|
||||
SADD "team:Knucklewimp" Belaeos Polytia Moritia
|
||||
SADD "team:Snarkdumbthimble" Daryos Blalios Ighteatia
|
||||
ZADD "challenges" 1 "Enter the hidden dungeon" 2 "Find the chicken coop" 3 "Defeat the goblins" 4 "Rescue the chickens" 5 "Escape the dungeon"
|
||||
76
go-code-samples/get-started-with-redis/transaction.go
Normal file
76
go-code-samples/get-started-with-redis/transaction.go
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// Task:
|
||||
// - Create a new team: Grumblebum
|
||||
// - Move Sykios, Nidios, and Belaeos to the new team
|
||||
// - Move Tiaitia to team Knucklewimp
|
||||
// - Remove team Dorkfoot
|
||||
|
||||
func transaction(client *redis.Client) error {
|
||||
ctx := context.Background()
|
||||
|
||||
_, err := client.TxPipelined(ctx, func(pipe redis.Pipeliner) error {
|
||||
// Move Sykios to team Grumblebum
|
||||
err := pipe.HSet(ctx, "player:1", "team", "Grumblebum").Err()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Move Nidios to team Grumblebum
|
||||
err = pipe.HSet(ctx, "player:2", "team", "Grumblebum").Err()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Move Belaeos to team Grumblebum
|
||||
err = pipe.HSet(ctx, "player:4", "team", "Grumblebum").Err()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Move Tiaitia to team Knucklewimp
|
||||
err = pipe.HSet(ctx, "player:3", "team", "Knucklewimp").Err()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Team update: remove Belaeos from team Knucklewimp
|
||||
err = pipe.SRem(ctx, "team:Knucklewimp", "Belaeos").Err()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Team update: add Tiaitia to team Knucklewimp
|
||||
err = pipe.SAdd(ctx, "team:Knucklewimp", "Tiaitia").Err()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Add team Grumblebum
|
||||
err = pipe.SAdd(ctx, "team:Grumblebum", "Sykios", "Nidios", "Belaeos").Err()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Remove team Dorkfoot. A set is removed by removing all elements.
|
||||
err = pipe.SRem(ctx, "team:Dorkfoot", "Sykios", "Nidios", "Tiaitia").Err()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("TxPipelined failed: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Sykios's new team: %s\n", client.HGet(ctx, "player:1", "team").Val())
|
||||
fmt.Printf("Belaeos's new team: %s\n", client.HGet(ctx, "player:4", "team").Val())
|
||||
fmt.Printf("Tiaitia's new team: %s\n", client.HGet(ctx, "player:3", "team").Val())
|
||||
fmt.Printf("Team Grumblebum: %s\n", client.SMembers(ctx, "team:Grumblebum").Val())
|
||||
fmt.Printf("Team Knucklewimp: %s\n", client.SMembers(ctx, "team:Knucklewimp").Val())
|
||||
return nil
|
||||
}
|
||||
5
go-code-samples/go-db-comparison/README.md
Normal file
5
go-code-samples/go-db-comparison/README.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# Comparing database/sql, GORM, sqlx, and sqlc
|
||||
|
||||
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.
|
||||
|
||||
Find the article [here](https://blog.jetbrains.com/go/2023/04/27/comparing-db-packages/).
|
||||
127
go-code-samples/go-db-comparison/benchmarks/benchmark.go
Normal file
127
go-code-samples/go-db-comparison/benchmarks/benchmark.go
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
package benchmarks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/jmoiron/sqlx"
|
||||
sqlc "github.com/rexfordnyrk/go-db-comparison/benchmarks/sqlc_generated"
|
||||
)
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
// Opening a database connection.
|
||||
db, err = sql.Open("mysql", "theuser:thepass@tcp(localhost:3306)/thedb?multiStatements=true&parseTime=true")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
//sqlx connection using existing db connection
|
||||
dbx = sqlx.NewDb(db, "mysql")
|
||||
|
||||
//sqlc connection using existing db connection
|
||||
dbc = sqlc.New(db)
|
||||
|
||||
//gorm connection using existing db connection
|
||||
gdb, err = gorm.Open(mysql.New(mysql.Config{Conn: db}))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
gdb *gorm.DB
|
||||
db *sql.DB
|
||||
dbx *sqlx.DB
|
||||
dbc *sqlc.Queries
|
||||
)
|
||||
|
||||
func setup() {
|
||||
clear()
|
||||
table := `CREATE TABLE students (
|
||||
id bigint NOT NULL AUTO_INCREMENT,
|
||||
fname varchar(50) not null,
|
||||
lname varchar(50) not null,
|
||||
date_of_birth datetime not null,
|
||||
email varchar(50) not null,
|
||||
address varchar(50) not null,
|
||||
gender varchar(50) not null,
|
||||
PRIMARY KEY (id)
|
||||
);`
|
||||
_, err := db.Exec(table)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
//inserting records
|
||||
_, err = db.Exec(records)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func clear() {
|
||||
_, err := db.Exec(`DROP TABLE IF EXISTS students;`)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
type Student struct {
|
||||
ID int64
|
||||
Fname string
|
||||
Lname string
|
||||
DateOfBirth time.Time `db:"date_of_birth"`
|
||||
Email string
|
||||
Address string
|
||||
Gender string
|
||||
}
|
||||
|
||||
func DbSqlQueryStudentWithLimit(limit int) {
|
||||
var students []Student
|
||||
rows, err := db.Query("SELECT * FROM students limit ?", limit)
|
||||
if err != nil {
|
||||
log.Fatalf("DbSqlQueryStudentWithLimit %d %v", limit, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
// Loop through rows, using Scan to assign column data to struct fields.
|
||||
for rows.Next() {
|
||||
var s Student
|
||||
if err := rows.Scan(&s.ID, &s.Fname, &s.Lname, &s.DateOfBirth, &s.Email, &s.Address, &s.Gender); err != nil {
|
||||
log.Fatalf("DbSqlQueryStudentWithLimit %d %v", limit, err)
|
||||
}
|
||||
students = append(students, s)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
log.Fatalf("DbSqlQueryStudentWithLimit %d %v", limit, err)
|
||||
}
|
||||
}
|
||||
|
||||
func SqlxQueryStudentWithLimit(limit int) {
|
||||
var students []Student
|
||||
err := dbx.Select(&students, "SELECT * FROM students LIMIT ?", limit)
|
||||
if err != nil {
|
||||
log.Fatalf("SqlxQueryStudentWithLimit %d %v", limit, err)
|
||||
}
|
||||
}
|
||||
|
||||
func SqlcQueryStudentWithLimit(limit int) {
|
||||
_, err := dbc.FetchStudents(context.Background(), int32(limit))
|
||||
if err != nil {
|
||||
log.Fatalf("SqlcQueryStudentWithLimit %d %v", limit, err)
|
||||
}
|
||||
}
|
||||
|
||||
func GormQueryStudentWithLimit(limit int) {
|
||||
var students []Student
|
||||
if err := gdb.Limit(limit).Find(&students).Error; err != nil {
|
||||
log.Fatalf("GormQueryStudentWithLimit %d %v", limit, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package benchmarks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Benchmark(b *testing.B) {
|
||||
setup()
|
||||
defer clear()
|
||||
|
||||
// Benchmark goes in here
|
||||
limits := []int{
|
||||
1,
|
||||
10,
|
||||
100,
|
||||
1000,
|
||||
10000,
|
||||
15000,
|
||||
}
|
||||
|
||||
for _, lim := range limits { // Fetch varying number of rows
|
||||
|
||||
fmt.Printf("================================== BENCHMARKING %d RECORDS ======================================\n", lim)
|
||||
// Benchmark Database/sql
|
||||
b.Run(fmt.Sprintf("Database/sql limit:%d ", lim), func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
DbSqlQueryStudentWithLimit(lim)
|
||||
}
|
||||
})
|
||||
|
||||
// Benchmark Sqlx
|
||||
b.Run(fmt.Sprintf("Sqlx limit:%d ", lim), func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
SqlxQueryStudentWithLimit(lim)
|
||||
}
|
||||
})
|
||||
|
||||
// Benchmark Sqlc
|
||||
b.Run(fmt.Sprintf("Sqlc limit:%d ", lim), func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
SqlcQueryStudentWithLimit(lim)
|
||||
}
|
||||
})
|
||||
|
||||
// Benchmark GORM
|
||||
b.Run(fmt.Sprintf("GORM limit:%d ", lim), func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
GormQueryStudentWithLimit(lim)
|
||||
}
|
||||
})
|
||||
|
||||
fmt.Println("=================================================================================================")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.16.0
|
||||
|
||||
package sqlc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
type DBTX interface {
|
||||
ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
|
||||
PrepareContext(context.Context, string) (*sql.Stmt, error)
|
||||
QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
|
||||
QueryRowContext(context.Context, string, ...interface{}) *sql.Row
|
||||
}
|
||||
|
||||
func New(db DBTX) *Queries {
|
||||
return &Queries{db: db}
|
||||
}
|
||||
|
||||
type Queries struct {
|
||||
db DBTX
|
||||
}
|
||||
|
||||
func (q *Queries) WithTx(tx *sql.Tx) *Queries {
|
||||
return &Queries{
|
||||
db: tx,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.16.0
|
||||
|
||||
package sqlc
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type Student struct {
|
||||
ID int64
|
||||
Fname string
|
||||
Lname string
|
||||
DateOfBirth time.Time
|
||||
Email string
|
||||
Address string
|
||||
Gender string
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
-- name: FetchStudents :many
|
||||
SELECT * FROM students LIMIT ?;
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.16.0
|
||||
// source: query.sql
|
||||
|
||||
package sqlc
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
const fetchStudents = `-- name: FetchStudents :many
|
||||
SELECT id, fname, lname, date_of_birth, email, address, gender FROM students LIMIT ?
|
||||
`
|
||||
|
||||
func (q *Queries) FetchStudents(ctx context.Context, limit int32) ([]Student, error) {
|
||||
rows, err := q.db.QueryContext(ctx, fetchStudents, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Student
|
||||
for rows.Next() {
|
||||
var i Student
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Fname,
|
||||
&i.Lname,
|
||||
&i.DateOfBirth,
|
||||
&i.Email,
|
||||
&i.Address,
|
||||
&i.Gender,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
CREATE TABLE `students` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||
`fname` varchar(50) not null,
|
||||
`lname` varchar(50) not null,
|
||||
`date_of_birth` datetime not null,
|
||||
`email` varchar(50) not null,
|
||||
`address` varchar(50) not null,
|
||||
`gender` varchar(50) not null,
|
||||
PRIMARY KEY (`id`)
|
||||
);
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
version: 1
|
||||
packages:
|
||||
- path: "./"
|
||||
name: "sqlc"
|
||||
engine: "mysql"
|
||||
schema: "schema.sql"
|
||||
queries: "query.sql"
|
||||
15003
go-code-samples/go-db-comparison/benchmarks/students.sql.go
Normal file
15003
go-code-samples/go-db-comparison/benchmarks/students.sql.go
Normal file
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,123 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
type Student struct {
|
||||
ID int
|
||||
Fname string
|
||||
Lname string
|
||||
DateOfBirth time.Time
|
||||
Email string
|
||||
Address string
|
||||
Gender string
|
||||
}
|
||||
|
||||
var db *sql.DB
|
||||
|
||||
func main() {
|
||||
var err error
|
||||
db, err = dbSqlConnect()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
s := Student{
|
||||
Fname: "Leon",
|
||||
Lname: "Ashling",
|
||||
DateOfBirth: time.Date(1994, time.August, 14, 23, 51, 42, 0, time.UTC),
|
||||
Email: "lashling5@senate.gov",
|
||||
Address: "39 Kipling Pass",
|
||||
Gender: "Male",
|
||||
}
|
||||
|
||||
//adding student record to table
|
||||
sID, err := addStudent(s)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
fmt.Printf("addSudent id: %v \n", sID)
|
||||
|
||||
//selecting student by ID
|
||||
st, err := studentByID(sID)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
fmt.Printf("studentByID id: %v \n", st)
|
||||
|
||||
students, err := fetchStudents()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
fmt.Printf("fetchStudents count: %v \n", len(students))
|
||||
}
|
||||
|
||||
func dbSqlConnect() (*sql.DB, error) {
|
||||
// Opening a database connection.
|
||||
db, err := sql.Open("mysql", "theuser:thepass@tcp(localhost:3306)/thedb?parseTime=true")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fmt.Println("Connected!")
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func addStudent(s Student) (int64, error) {
|
||||
query := "insert into students (fname, lname, date_of_birth, email, gender, address) values (?, ?, ?, ?, ?, ?);"
|
||||
result, err := db.Exec(query, s.Fname, s.Lname, s.DateOfBirth, s.Email, s.Gender, s.Address)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("addStudent Error: %v", err)
|
||||
}
|
||||
|
||||
id, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("addSudent Error: %v", err)
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func fetchStudents() ([]Student, error) {
|
||||
// A slice of Students to hold data from returned rows.
|
||||
var students []Student
|
||||
|
||||
rows, err := db.Query("SELECT * FROM students")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fetchStudents %v", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
// Loop through rows, using Scan to assign column data to struct fields.
|
||||
for rows.Next() {
|
||||
var s Student
|
||||
if err := rows.Scan(&s.ID, &s.Fname, &s.Lname, &s.DateOfBirth, &s.Email, &s.Address, &s.Gender); err != nil {
|
||||
return nil, fmt.Errorf("fetchStudents %v", err)
|
||||
}
|
||||
students = append(students, s)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("fetchStudents %v", err)
|
||||
}
|
||||
|
||||
return students, nil
|
||||
}
|
||||
|
||||
func studentByID(id int64) (Student, error) {
|
||||
var st Student
|
||||
|
||||
row := db.QueryRow("SELECT * FROM students WHERE id = ?", id)
|
||||
if err := row.Scan(&st.ID, &st.Fname, &st.Lname, &st.DateOfBirth, &st.Email, &st.Address, &st.Gender); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return st, fmt.Errorf("studentById %d: no such student", id)
|
||||
}
|
||||
return st, fmt.Errorf("studentById %d: %v", id, err)
|
||||
}
|
||||
return st, nil
|
||||
}
|
||||
48
go-code-samples/go-db-comparison/examples/gorm/gorm.go
Normal file
48
go-code-samples/go-db-comparison/examples/gorm/gorm.go
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Student struct {
|
||||
ID int
|
||||
Fname string
|
||||
Lname string
|
||||
DateOfBirth time.Time
|
||||
Email string
|
||||
Address string
|
||||
Gender string
|
||||
}
|
||||
|
||||
func main() {
|
||||
// refer https://github.com/go-sql-driver/mysql#dsn-data-source-name for details
|
||||
db, err := gorm.Open(mysql.Open("theuser:thepass@tcp(127.0.0.1:3306)/thedb?charset=utf8mb4&parseTime=True&loc=Local"))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Println("Connected!")
|
||||
|
||||
//initializing record to be inserted
|
||||
s := Student{
|
||||
Fname: "Leon",
|
||||
Lname: "Ashling",
|
||||
DateOfBirth: time.Date(1994, time.August, 14, 23, 51, 42, 0, time.UTC),
|
||||
Email: "lashling5@senate.gov",
|
||||
Address: "39 Kipling Pass",
|
||||
Gender: "Male",
|
||||
}
|
||||
//adds student record and returns the ID into the ID field
|
||||
db.Create(&s)
|
||||
fmt.Printf("addSudent id: %v \n", s.ID)
|
||||
|
||||
//selecting multiple record
|
||||
var students []Student
|
||||
db.Limit(10).Find(&students)
|
||||
fmt.Printf("fetchStudents count: %v \n", len(students))
|
||||
}
|
||||
22
go-code-samples/go-db-comparison/examples/setup.sql
Normal file
22
go-code-samples/go-db-comparison/examples/setup.sql
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
DROP TABLE IF EXISTS students;
|
||||
CREATE TABLE `students` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||
`fname` varchar(50) not null,
|
||||
`lname` varchar(50) not null,
|
||||
`date_of_birth` datetime not null,
|
||||
`email` varchar(50),
|
||||
`address` varchar(50),
|
||||
`gender` varchar(50),
|
||||
PRIMARY KEY (`id`)
|
||||
);
|
||||
|
||||
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');
|
||||
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');
|
||||
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');
|
||||
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');
|
||||
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');
|
||||
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');
|
||||
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');
|
||||
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');
|
||||
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');
|
||||
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');
|
||||
31
go-code-samples/go-db-comparison/examples/sqlc/db.go
Normal file
31
go-code-samples/go-db-comparison/examples/sqlc/db.go
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.17.2
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
type DBTX interface {
|
||||
ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
|
||||
PrepareContext(context.Context, string) (*sql.Stmt, error)
|
||||
QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
|
||||
QueryRowContext(context.Context, string, ...interface{}) *sql.Row
|
||||
}
|
||||
|
||||
func New(db DBTX) *Queries {
|
||||
return &Queries{db: db}
|
||||
}
|
||||
|
||||
type Queries struct {
|
||||
db DBTX
|
||||
}
|
||||
|
||||
func (q *Queries) WithTx(tx *sql.Tx) *Queries {
|
||||
return &Queries{
|
||||
db: tx,
|
||||
}
|
||||
}
|
||||
19
go-code-samples/go-db-comparison/examples/sqlc/models.go
Normal file
19
go-code-samples/go-db-comparison/examples/sqlc/models.go
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.17.2
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type Student struct {
|
||||
ID int64
|
||||
Fname string
|
||||
Lname string
|
||||
DateOfBirth time.Time
|
||||
Email string
|
||||
Address string
|
||||
Gender string
|
||||
}
|
||||
8
go-code-samples/go-db-comparison/examples/sqlc/query.sql
Normal file
8
go-code-samples/go-db-comparison/examples/sqlc/query.sql
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
-- name: addStudent :execlastid
|
||||
insert into students (fname, lname, date_of_birth, email, gender, address) values (?, ?, ?, ?, ?, ?);
|
||||
|
||||
-- name: studentByID :one
|
||||
SELECT * FROM students WHERE id = ?;
|
||||
|
||||
-- name: fetchStudents :many
|
||||
SELECT * FROM students LIMIT 10;
|
||||
93
go-code-samples/go-db-comparison/examples/sqlc/query.sql.go
Normal file
93
go-code-samples/go-db-comparison/examples/sqlc/query.sql.go
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.17.2
|
||||
// source: query.sql
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
const addStudent = `-- name: addStudent :execlastid
|
||||
insert into students (fname, lname, date_of_birth, email, gender, address) values (?, ?, ?, ?, ?, ?)
|
||||
`
|
||||
|
||||
type addStudentParams struct {
|
||||
Fname string
|
||||
Lname string
|
||||
DateOfBirth time.Time
|
||||
Email string
|
||||
Gender string
|
||||
Address string
|
||||
}
|
||||
|
||||
func (q *Queries) addStudent(ctx context.Context, arg addStudentParams) (int64, error) {
|
||||
result, err := q.db.ExecContext(ctx, addStudent,
|
||||
arg.Fname,
|
||||
arg.Lname,
|
||||
arg.DateOfBirth,
|
||||
arg.Email,
|
||||
arg.Gender,
|
||||
arg.Address,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return result.LastInsertId()
|
||||
}
|
||||
|
||||
const fetchStudents = `-- name: fetchStudents :many
|
||||
SELECT id, fname, lname, date_of_birth, email, address, gender FROM students LIMIT 10
|
||||
`
|
||||
|
||||
func (q *Queries) fetchStudents(ctx context.Context) ([]Student, error) {
|
||||
rows, err := q.db.QueryContext(ctx, fetchStudents)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Student
|
||||
for rows.Next() {
|
||||
var i Student
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Fname,
|
||||
&i.Lname,
|
||||
&i.DateOfBirth,
|
||||
&i.Email,
|
||||
&i.Address,
|
||||
&i.Gender,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const studentByID = `-- name: studentByID :one
|
||||
SELECT id, fname, lname, date_of_birth, email, address, gender FROM students WHERE id = ?
|
||||
`
|
||||
|
||||
func (q *Queries) studentByID(ctx context.Context, id int64) (Student, error) {
|
||||
row := q.db.QueryRowContext(ctx, studentByID, id)
|
||||
var i Student
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Fname,
|
||||
&i.Lname,
|
||||
&i.DateOfBirth,
|
||||
&i.Email,
|
||||
&i.Address,
|
||||
&i.Gender,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
10
go-code-samples/go-db-comparison/examples/sqlc/schema.sql
Normal file
10
go-code-samples/go-db-comparison/examples/sqlc/schema.sql
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
CREATE TABLE `students` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||
`fname` varchar(50) not null,
|
||||
`lname` varchar(50) not null,
|
||||
`date_of_birth` datetime not null,
|
||||
`email` varchar(50) not null,
|
||||
`address` varchar(50) not null,
|
||||
`gender` varchar(50) not null,
|
||||
PRIMARY KEY (`id`)
|
||||
);
|
||||
52
go-code-samples/go-db-comparison/examples/sqlc/sqlc.go
Normal file
52
go-code-samples/go-db-comparison/examples/sqlc/sqlc.go
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
func main() {
|
||||
conn, err := sql.Open("mysql", "theuser:thepass@tcp(localhost:3306)/thedb?parseTime=true")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println("Connected!")
|
||||
|
||||
db := New(conn)
|
||||
|
||||
//initializing record to be inserted
|
||||
newSt := addStudentParams{
|
||||
Fname: "Leon",
|
||||
Lname: "Ashling",
|
||||
DateOfBirth: time.Date(1994, time.August, 14, 23, 51, 42, 0, time.UTC),
|
||||
Email: "lashling5@senate.gov",
|
||||
Gender: "Male",
|
||||
Address: "39 Kipling Pass",
|
||||
}
|
||||
// inserting the record
|
||||
sID, err := db.addStudent(context.Background(), newSt)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("addSudent id: %v \n", sID)
|
||||
|
||||
//retreive record by id
|
||||
st, err := db.studentByID(context.Background(), sID)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
fmt.Printf("studentByID record: %v \n", st)
|
||||
|
||||
//fetching multiple records
|
||||
students, err := db.fetchStudents(context.Background())
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
|
||||
fmt.Printf("fetchStudents count: %v \n", len(students))
|
||||
}
|
||||
7
go-code-samples/go-db-comparison/examples/sqlc/sqlc.yaml
Normal file
7
go-code-samples/go-db-comparison/examples/sqlc/sqlc.yaml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
version: 1
|
||||
packages:
|
||||
- path: "./"
|
||||
name: "main"
|
||||
engine: "mysql"
|
||||
schema: "schema.sql"
|
||||
queries: "query.sql"
|
||||
114
go-code-samples/go-db-comparison/examples/sqlx/sqlx.go
Normal file
114
go-code-samples/go-db-comparison/examples/sqlx/sqlx.go
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
type Student struct {
|
||||
ID int
|
||||
Fname string
|
||||
Lname string
|
||||
DateOfBirth time.Time `db:"date_of_birth"`
|
||||
Email string
|
||||
Address string
|
||||
Gender string
|
||||
}
|
||||
|
||||
var db *sqlx.DB
|
||||
|
||||
func main() {
|
||||
var err error
|
||||
db, err = sqlxConnect()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
s := Student{
|
||||
Fname: "Leon",
|
||||
Lname: "Ashling",
|
||||
DateOfBirth: time.Date(1994, time.August, 14, 23, 51, 42, 0, time.UTC),
|
||||
Email: "lashling5@senate.gov",
|
||||
Address: "39 Kipling Pass",
|
||||
Gender: "Male",
|
||||
}
|
||||
|
||||
//adding student record to table
|
||||
sID, err := addStudent(s)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
fmt.Printf("addStudent id: %v \n", sID)
|
||||
|
||||
//selecting student by ID
|
||||
st, err := studentByID(sID)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
fmt.Printf("studentByID record: %v \n", st)
|
||||
|
||||
students, err := fetchStudents()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
fmt.Printf("fetchStudents count: %v \n", len(students))
|
||||
}
|
||||
|
||||
func sqlxConnect() (*sqlx.DB, error) {
|
||||
// Opening a database connection.
|
||||
db, err := sqlx.Open("mysql", "theuser:thepass@tcp(localhost:3306)/thedb?parseTime=true")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fmt.Println("Connected!")
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func addStudent(s Student) (int64, error) {
|
||||
query := "insert into students (fname, lname, date_of_birth, email, gender, address) values (?, ?, ?, ?, ?, ?);"
|
||||
result := db.MustExec(query, s.Fname, s.Lname, s.DateOfBirth, s.Email, s.Gender, s.Address)
|
||||
|
||||
id, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("addSudent Error: %v", err)
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func fetchStudents() ([]Student, error) {
|
||||
// A slice of Students to hold data from returned rows.
|
||||
var students []Student
|
||||
|
||||
err := db.Select(&students, "SELECT * FROM students LIMIT 10")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fetchStudents %v", err)
|
||||
}
|
||||
|
||||
return students, nil
|
||||
}
|
||||
|
||||
func studentByID(id int64) (Student, error) {
|
||||
var st Student
|
||||
|
||||
//if err := db.QueryRowx("SELECT * FROM students WHERE id = ?", id).StructScan(&st); err != nil {
|
||||
// if err == sql.ErrNoRows {
|
||||
// return st, fmt.Errorf("studentById %d: no such student", id)
|
||||
// }
|
||||
// return st, fmt.Errorf("studentById %d: %v", id, err)
|
||||
//}
|
||||
|
||||
if err := db.Get(&st, "SELECT * FROM students WHERE id = ?", id); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return st, fmt.Errorf("studentById %d: no such student", id)
|
||||
}
|
||||
return st, fmt.Errorf("studentById %d: %v", id, err)
|
||||
}
|
||||
return st, nil
|
||||
}
|
||||
10
go-code-samples/go-db-comparison/go.mod
Normal file
10
go-code-samples/go-db-comparison/go.mod
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
module github.com/rexfordnyrk/go-db-comparison
|
||||
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
github.com/go-sql-driver/mysql v1.6.0
|
||||
github.com/jmoiron/sqlx v1.3.5
|
||||
gorm.io/driver/mysql v1.4.4
|
||||
gorm.io/gorm v1.24.6
|
||||
)
|
||||
18
go-code-samples/go-db-comparison/go.sum
Normal file
18
go-code-samples/go-db-comparison/go.sum
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
|
||||
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
|
||||
github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
|
||||
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
gorm.io/driver/mysql v1.4.4 h1:MX0K9Qvy0Na4o7qSC/YI7XxqUw5KDw01umqgID+svdQ=
|
||||
gorm.io/driver/mysql v1.4.4/go.mod h1:BCg8cKI+R0j/rZRQxeKis/forqRwRSYOR8OM3Wo6hOM=
|
||||
gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
|
||||
gorm.io/gorm v1.24.6 h1:wy98aq9oFEetsc4CAbKD2SoBCdMzsbSIvSUUFJuHi5s=
|
||||
gorm.io/gorm v1.24.6/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
|
||||
42
go-code-samples/go-gin-react/go-gin-react-part1/go.mod
Normal file
42
go-code-samples/go-gin-react/go-gin-react-part1/go.mod
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
module chat-app
|
||||
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/glebarez/go-sqlite v1.21.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.9.1 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.14.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
||||
github.com/leodido/go-urn v1.2.4 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
golang.org/x/crypto v0.14.0 // indirect
|
||||
golang.org/x/net v0.17.0 // indirect
|
||||
golang.org/x/sys v0.13.0 // indirect
|
||||
golang.org/x/text v0.13.0 // indirect
|
||||
google.golang.org/protobuf v1.30.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/libc v1.22.3 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
modernc.org/memory v1.5.0 // indirect
|
||||
modernc.org/sqlite v1.21.1 // indirect
|
||||
)
|
||||
103
go-code-samples/go-gin-react/go-gin-react-part1/go.sum
Normal file
103
go-code-samples/go-gin-react/go-gin-react-part1/go.sum
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
||||
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
||||
github.com/glebarez/go-sqlite v1.21.1 h1:7MZyUPh2XTrHS7xNEHQbrhfMZuPSzhkm2A1qgg0y5NY=
|
||||
github.com/glebarez/go-sqlite v1.21.1/go.mod h1:ISs8MF6yk5cL4n/43rSOmVMGJJjHYr7L2MbZZ5Q4E2E=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
|
||||
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
|
||||
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
||||
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
modernc.org/libc v1.22.3 h1:D/g6O5ftAfavceqlLOFwaZuA5KYafKwmr30A6iSqoyY=
|
||||
modernc.org/libc v1.22.3/go.mod h1:MQrloYP209xa2zHome2a8HLiLm6k0UT8CoHpV74tOFw=
|
||||
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
|
||||
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
|
||||
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
||||
modernc.org/sqlite v1.21.1 h1:GyDFqNnESLOhwwDRaHGdp2jKLDzpyT/rNLglX3ZkMSU=
|
||||
modernc.org/sqlite v1.21.1/go.mod h1:XwQ0wZPIh1iKb5mkvCJ3szzbhk+tykC8ZWqTRTgYRwI=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
271
go-code-samples/go-gin-react/go-gin-react-part1/main.go
Normal file
271
go-code-samples/go-gin-react/go-gin-react-part1/main.go
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
_ "github.com/glebarez/go-sqlite"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
ID int `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type Channel struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
ID int `json:"id"`
|
||||
ChannelID int `json:"channel_id"`
|
||||
UserID int `json:"user_id"`
|
||||
UserName string `json:"user_name"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Get the working directory
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// Print the working directory
|
||||
fmt.Println("Working directory:", wd)
|
||||
|
||||
// Open the SQLite database file
|
||||
db, err := sql.Open("sqlite", wd+"/database.db")
|
||||
|
||||
defer func(db *sql.DB) {
|
||||
err := db.Close()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}(db)
|
||||
|
||||
// Create the Gin router
|
||||
r := gin.Default()
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Creation endpoints
|
||||
r.POST("/users", func(c *gin.Context) { createUser(c, db) })
|
||||
r.POST("/channels", func(c *gin.Context) { createChannel(c, db) })
|
||||
r.POST("/messages", func(c *gin.Context) { createMessage(c, db) })
|
||||
|
||||
// Listing endpoints
|
||||
r.GET("/channels", func(c *gin.Context) { listChannels(c, db) })
|
||||
r.GET("/messages", func(c *gin.Context) { listMessages(c, db) })
|
||||
|
||||
// Login endpoint
|
||||
r.POST("/login", func(c *gin.Context) { login(c, db) })
|
||||
|
||||
err = r.Run(":8080")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// User creation endpoint
|
||||
func createUser(c *gin.Context, db *sql.DB) {
|
||||
// Parse JSON request body into User struct
|
||||
var user User
|
||||
if err := c.ShouldBindJSON(&user); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Insert user into database
|
||||
result, err := db.Exec("INSERT INTO users (username, password) VALUES (?, ?)", user.Username, user.Password)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Get ID of newly inserted user
|
||||
id, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Return ID of newly inserted user
|
||||
c.JSON(http.StatusOK, gin.H{"id": id})
|
||||
}
|
||||
|
||||
// Login endpoint
|
||||
func login(c *gin.Context, db *sql.DB) {
|
||||
// Parse JSON request body into User struct
|
||||
var user User
|
||||
if err := c.ShouldBindJSON(&user); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Query database for user
|
||||
row := db.QueryRow("SELECT id FROM users WHERE username = ? AND password = ?", user.Username, user.Password)
|
||||
|
||||
// Get ID of user
|
||||
var id int
|
||||
err := row.Scan(&id)
|
||||
if err != nil {
|
||||
// Check if user was not found
|
||||
if err == sql.ErrNoRows {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid username or password"})
|
||||
return
|
||||
}
|
||||
// Return error if other error occurred
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
}
|
||||
|
||||
// Return ID of user
|
||||
c.JSON(http.StatusOK, gin.H{"id": id})
|
||||
}
|
||||
|
||||
// Channel creation endpoint
|
||||
func createChannel(c *gin.Context, db *sql.DB) {
|
||||
// Parse JSON request body into Channel struct
|
||||
var channel Channel
|
||||
if err := c.ShouldBindJSON(&channel); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Insert channel into database
|
||||
result, err := db.Exec("INSERT INTO channels (name) VALUES (?)", channel.Name)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Get ID of newly inserted channel
|
||||
id, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Return ID of newly inserted channel
|
||||
c.JSON(http.StatusOK, gin.H{"id": id})
|
||||
}
|
||||
|
||||
// Channel listing endpoint
|
||||
func listChannels(c *gin.Context, db *sql.DB) {
|
||||
// Query database for channels
|
||||
rows, err := db.Query("SELECT id, name FROM channels")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Create slice of channels
|
||||
var channels []Channel
|
||||
|
||||
// Iterate over rows
|
||||
for rows.Next() {
|
||||
// Create new channel
|
||||
var channel Channel
|
||||
|
||||
// Scan row into channel
|
||||
err := rows.Scan(&channel.ID, &channel.Name)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Append channel to slice
|
||||
channels = append(channels, channel)
|
||||
}
|
||||
|
||||
// Return slice of channels
|
||||
c.JSON(http.StatusOK, channels)
|
||||
}
|
||||
|
||||
// Message creation endpoint
|
||||
func createMessage(c *gin.Context, db *sql.DB) {
|
||||
// Parse JSON request body into Message struct
|
||||
var message Message
|
||||
if err := c.ShouldBindJSON(&message); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Insert message into database
|
||||
result, err := db.Exec("INSERT INTO messages (channel_id, user_id, message) VALUES (?, ?, ?)", message.ChannelID, message.UserID, message.Text)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Get ID of newly inserted message
|
||||
id, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Return ID of newly inserted message
|
||||
c.JSON(http.StatusOK, gin.H{"id": id})
|
||||
}
|
||||
|
||||
// Message listing endpoint
|
||||
func listMessages(c *gin.Context, db *sql.DB) {
|
||||
// Parse channel ID from URL
|
||||
channelID, err := strconv.Atoi(c.Query("channelID"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Parse optional limit query parameter from URL
|
||||
limit, err := strconv.Atoi(c.Query("limit"))
|
||||
if err != nil {
|
||||
// Set limit to 100 if not provided
|
||||
limit = 100
|
||||
}
|
||||
|
||||
// Parse last message ID query parameter from URL. This is used to get messages after a certain message.
|
||||
lastMessageID, err := strconv.Atoi(c.Query("lastMessageID"))
|
||||
if err != nil {
|
||||
// Set last message ID to 0 if not provided
|
||||
lastMessageID = 0
|
||||
}
|
||||
|
||||
// Query database for messages
|
||||
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)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Create slice of messages
|
||||
var messages []Message
|
||||
|
||||
// Iterate over rows
|
||||
for rows.Next() {
|
||||
// Create new message
|
||||
var message Message
|
||||
|
||||
// Scan row into message
|
||||
err := rows.Scan(&message.ID, &message.ChannelID, &message.UserID, &message.UserName, &message.Text)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Append message to slice
|
||||
messages = append(messages, message)
|
||||
}
|
||||
|
||||
// Return slice of messages
|
||||
c.JSON(http.StatusOK, messages)
|
||||
}
|
||||
18
go-code-samples/go-gin-react/go-gin-react-part1/schema.sql
Normal file
18
go-code-samples/go-gin-react/go-gin-react-part1/schema.sql
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
CREATE TABLE users (
|
||||
id INTEGER PRIMARY KEY,
|
||||
username TEXT NOT NULL,
|
||||
password TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE channels (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE messages (
|
||||
id INTEGER PRIMARY KEY,
|
||||
channel_id INTEGER NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
message TEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
23
go-code-samples/go-gin-react/go-gin-react-part2/chat-ui/.gitignore
vendored
Normal file
23
go-code-samples/go-gin-react/go-gin-react-part2/chat-ui/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
17511
go-code-samples/go-gin-react/go-gin-react-part2/chat-ui/package-lock.json
generated
Normal file
17511
go-code-samples/go-gin-react/go-gin-react-part2/chat-ui/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"name": "chat-ui",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"react-router-dom": "^6.13.0"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
|
|
@ -0,0 +1,44 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@latest/dist/tailwind.min.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 9.4 KiB |
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import React from 'react';
|
||||
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
|
||||
import Login from './Login';
|
||||
import CreateUser from './CreateUser';
|
||||
import MainChat from './MainChat';
|
||||
|
||||
const App = () => {
|
||||
return (
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/create-user" element={<CreateUser />} />
|
||||
<Route path="/chat" element={<MainChat />} />
|
||||
<Route path="/chat/:channelId" element={<MainChat />} />
|
||||
<Route path="/" element={<Login />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import {useParams} from "react-router-dom";
|
||||
|
||||
const ChannelsList = ({ selectedChannel, setSelectedChannel }) => {
|
||||
const { channelId } = useParams();
|
||||
const [channels, setChannels] = useState([]);
|
||||
const [newChannelName, setNewChannelName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (channelId) {
|
||||
const channel = channels.find((channel) => channel.id === parseInt(channelId));
|
||||
if (channel) {
|
||||
setSelectedChannel({name: channel.name, id: parseInt(channelId)});
|
||||
}
|
||||
}
|
||||
}, [channelId, channels]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchChannels = async () => {
|
||||
const response = await fetch('/channels');
|
||||
const data = await response.json();
|
||||
setChannels(data || []);
|
||||
};
|
||||
fetchChannels();
|
||||
}, []);
|
||||
|
||||
const handleAddChannel = async () => {
|
||||
const response = await fetch('/channels', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: newChannelName }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const newChannel = await response.json();
|
||||
setChannels([...channels, { id: newChannel.id, name: newChannelName }]);
|
||||
setNewChannelName('');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full bg-gray-100 border-r">
|
||||
<div className="bg-gray-700 text-white p-2">
|
||||
Channels
|
||||
</div>
|
||||
<div className="overflow-y-auto flex-grow p-4">
|
||||
{channels ? (
|
||||
<ul className="w-full">
|
||||
{channels.map((channel) => (
|
||||
<li
|
||||
key={channel.id}
|
||||
className={`p-2 rounded-md w-full cursor-pointer ${parseInt(channelId) === channel.id ? 'bg-blue-500 text-white' : 'hover:bg-gray-200'}`}
|
||||
onClick={() => setSelectedChannel(channel)}
|
||||
>
|
||||
{channel.name}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<div className="text-center text-gray-600">
|
||||
Please add a Channel
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col p-4">
|
||||
<input
|
||||
type="text"
|
||||
value={newChannelName}
|
||||
onChange={(e) => setNewChannelName(e.target.value)}
|
||||
placeholder="New channel..."
|
||||
className="mb-4 p-2 w-full border rounded-md bg-white"
|
||||
/>
|
||||
<button onClick={handleAddChannel} className="p-2 bg-blue-500 text-white rounded-md">Add Channel</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChannelsList;
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
import React, { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
const CreateUser = () => {
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const response = await fetch('/users', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
navigate('/');
|
||||
} else {
|
||||
alert('Account creation failed');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
||||
<form onSubmit={handleSubmit} className="p-8 border rounded shadow-md">
|
||||
<div className="mb-4">
|
||||
<label htmlFor="username" className="block text-sm font-medium text-gray-700">Username</label>
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
className="mt-1 p-2 w-full border rounded-md"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label htmlFor="password" className="block text-sm font-medium text-gray-700">Password</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="mt-1 p-2 w-full border rounded-md"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<button type="submit" className="w-full p-2 bg-blue-500 text-white rounded-md">Create Account</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateUser;
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
import React, { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
const Login = () => {
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const response = await fetch('/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
localStorage.setItem('userId', data.id);
|
||||
localStorage.setItem('userName', username);
|
||||
navigate('/chat');
|
||||
} else {
|
||||
alert('Login failed');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
||||
<form onSubmit={handleSubmit} className="p-8 border rounded shadow-md">
|
||||
<div className="mb-4">
|
||||
<label htmlFor="username" className="block text-sm font-medium text-gray-700">Username</label>
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
className="mt-1 p-2 w-full border rounded-md"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label htmlFor="password" className="block text-sm font-medium text-gray-700">Password</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="mt-1 p-2 w-full border rounded-md"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<button type="submit" className="w-full p-2 bg-blue-500 text-white rounded-md">Log In</button>
|
||||
<div className="mt-4 text-center">
|
||||
<span className="text-sm text-gray-600">Don't have an account? </span>
|
||||
<a href="/create-user" className="text-blue-500 hover:underline">Create one</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import ChannelsList from './ChannelsList';
|
||||
import MessagesPanel from './MessagesPanel';
|
||||
|
||||
const MainChat = () => {
|
||||
const { channelId } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const [selectedChannel, setSelectedChannel] = useState(parseInt(channelId) || null);
|
||||
|
||||
// If the component loads with a channel ID in the URL, set it as the selected channel.
|
||||
useEffect(() => {
|
||||
if (selectedChannel) {
|
||||
navigate(`/chat/${selectedChannel.id}`);
|
||||
}
|
||||
}, [selectedChannel, navigate]);
|
||||
|
||||
const handleChannelSelect = (channelId) => {
|
||||
setSelectedChannel(channelId);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-screen">
|
||||
<div className="w-1/4 border-r">
|
||||
<ChannelsList selectedChannel={selectedChannel} setSelectedChannel={handleChannelSelect} />
|
||||
</div>
|
||||
<div className="w-3/4">
|
||||
<MessagesPanel selectedChannel={selectedChannel} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MainChat;
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
import React, { useState } from 'react';
|
||||
|
||||
const MessageEntry = ({ selectedChannel, onNewMessage }) => {
|
||||
const [text, setText] = useState('');
|
||||
|
||||
const handleSendMessage = async () => {
|
||||
const userID = localStorage.getItem('userId');
|
||||
const userName = localStorage.getItem('userName');
|
||||
|
||||
const response = await fetch('/messages', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
"channel_id": parseInt(selectedChannel.id),
|
||||
"user_id": parseInt(userID),
|
||||
text
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const message = await response.json();
|
||||
onNewMessage({
|
||||
id: message.id,
|
||||
channel_id: selectedChannel,
|
||||
user_id: userID,
|
||||
user_name: userName,
|
||||
text
|
||||
});
|
||||
setText('');
|
||||
} else {
|
||||
alert('Failed to send message');
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (event) => {
|
||||
if (event.key === 'Enter' && !event.shiftKey) {
|
||||
handleSendMessage();
|
||||
event.preventDefault(); // Prevent the default behavior (newline)
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-4 border-t flex">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Type a message..."
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
className="p-2 flex-grow border rounded-md mr-2"
|
||||
/>
|
||||
<button
|
||||
onClick={handleSendMessage}
|
||||
className="p-2 bg-blue-500 text-white rounded-md"
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MessageEntry;
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import MessageEntry from './MessageEntry';
|
||||
|
||||
const MessagesPanel = ({ selectedChannel }) => {
|
||||
const [messages, setMessages] = useState([]);
|
||||
const lastMessageIdRef = useRef(null); // Keep track of the last message ID
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedChannel) return;
|
||||
|
||||
let isMounted = true; // flag to prevent state updates after unmount
|
||||
let intervalId = null;
|
||||
|
||||
const fetchMessages = async () => {
|
||||
const response = await fetch(`/messages?channelID=${selectedChannel.id}`);
|
||||
const data = await response.json();
|
||||
if (isMounted) {
|
||||
let messageData = data || [];
|
||||
setMessages(messageData);
|
||||
lastMessageIdRef.current = messageData.length > 0 ? messageData[messageData.length - 1].id : null;
|
||||
}
|
||||
};
|
||||
|
||||
fetchMessages();
|
||||
|
||||
intervalId = setInterval(() => {
|
||||
if (lastMessageIdRef.current !== null) {
|
||||
fetch(`/messages?channelID=${selectedChannel.id}&lastMessageID=${lastMessageIdRef.current}`)
|
||||
.then(response => response.json())
|
||||
.then(newMessages => {
|
||||
if (isMounted && Array.isArray(newMessages) && newMessages.length > 0) {
|
||||
setMessages((messages) => {
|
||||
const updatedMessages = [...messages, ...newMessages];
|
||||
lastMessageIdRef.current = updatedMessages[updatedMessages.length - 1].id;
|
||||
return updatedMessages;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}, 5000); // Poll every 5 seconds
|
||||
|
||||
return () => {
|
||||
isMounted = false; // prevent further state updates
|
||||
clearInterval(intervalId); // clear interval on unmount
|
||||
};
|
||||
}, [selectedChannel]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
{selectedChannel && (
|
||||
<div className="bg-gray-700 text-white p-2">
|
||||
Messages for {selectedChannel.name}
|
||||
</div>
|
||||
)}
|
||||
<div className={`overflow-auto flex-grow ${selectedChannel && messages.length === 0 ? 'flex items-center justify-center' : ''}`}>
|
||||
{selectedChannel ? (
|
||||
messages.length > 0 ? (
|
||||
messages.map((message) => (
|
||||
<div key={message.id} className="p-2 border-b">
|
||||
<strong>{message.user_name}</strong>: {message.text}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="text-center text-gray-600">
|
||||
No messages yet! Why not send one?
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="p-2">Please select a channel</div>
|
||||
)}
|
||||
</div>
|
||||
{selectedChannel && (
|
||||
<MessageEntry
|
||||
selectedChannel={selectedChannel}
|
||||
onNewMessage={(message) => {
|
||||
lastMessageIdRef.current = message.id;
|
||||
setMessages([...messages, message])
|
||||
}
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MessagesPanel;
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user