Download and save bulk URL concurrently
$begingroup$
I am new to Go and wrote program to download and save bulk URLs concurrently. It is working correctly, but I would like to make it more efficient and follow best practices.
package main
import (...)
// Connection String
const connectionString = "connectionString"
// Declare Channels
var wg sync.WaitGroup
var ch = make(chan *item) // For success item(s)
var errItems = make(chan *item) // For error item(s)
type item struct {
id int
url string
path string
content byte
err error
}
func main() {
start := time.Now()
var basePath = os.Args[1]
var from, to = os.Args[2], os.Args[3]
db, err := sql.Open("sqlserver", connectionString)
if err != nil {
log.Fatal(err.Error())
}
rows, err := db.Query("SELECT [ImageID],[AccommodationlID],[Link],[FileName] FROM [dbo].[AccomodationImage] where AccommodationlID between @from and @to and FileName not like '%NoHotel.png%'", sql.Named("from", from), sql.Named("to", to))
if err != nil {
log.Fatal(err.Error())
}
defer db.Close()
for rows.Next() {
var item = item{}
var accId, name string
_ = rows.Scan(&item.id, &accId, &item.url, &name)
item.path = fmt.Sprintf("%s\%s\%s", basePath, accId, name)
wg.Add(1)
go downloadFile(&item)
go func() {
select {
case done := <-ch:
wg.Add(1)
go saveAndUpdateFile(db, done)
case errorItem := <-errItems:
wg.Add(1)
go printResult(errorItem)
}
}()
}
wg.Wait()
fmt.Printf("%.2fs elapsedn", time.Since(start).Seconds())
}
func downloadFile(item *item) {
defer wg.Done()
resp, err := http.Get(item.url)
if err != nil {
item.content, item.err = nil, err
} else if resp.StatusCode != http.StatusOK {
item.content, item.err = nil, errors.New(resp.Status)
} else {
item.content, item.err = ioutil.ReadAll(resp.Body)
}
if item.err != nil {
errItems <- item
} else {
ch <- item
}
}
func saveAndUpdateFile(db *sql.DB, item *item) {
defer wg.Done()
if item.content == nil {
item.err = errors.New("Content is empty.")
} else {
dir := filepath.Dir(item.path)
err := os.MkdirAll(dir, os.ModePerm)
if err != nil {
item.err = err
} else {
item.err = ioutil.WriteFile(item.path, item.content, 0644)
}
}
if item.err == nil {
result, err := db.Exec("UPDATE [dbo].[AccomodationImage] SET IsRead = 1 WHERE ImageID = @id", sql.Named("id", item.id))
if rows, _ := result.RowsAffected(); rows <= 0 || err != nil {
item.err = errors.New("Update status failed.")
}
}
if item.err != nil {
errItems <- item
}
}
func printResult(item *item) {
defer wg.Done()
fmt.Println(item.toString())
}
I use a select statement to implement event so that when errItems or ch channels receive, save or print items. I also surrounded select statement with goroutine to prevent blocking.
sql go concurrency
New contributor
MatinMoezi is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.
$endgroup$
add a comment |
$begingroup$
I am new to Go and wrote program to download and save bulk URLs concurrently. It is working correctly, but I would like to make it more efficient and follow best practices.
package main
import (...)
// Connection String
const connectionString = "connectionString"
// Declare Channels
var wg sync.WaitGroup
var ch = make(chan *item) // For success item(s)
var errItems = make(chan *item) // For error item(s)
type item struct {
id int
url string
path string
content byte
err error
}
func main() {
start := time.Now()
var basePath = os.Args[1]
var from, to = os.Args[2], os.Args[3]
db, err := sql.Open("sqlserver", connectionString)
if err != nil {
log.Fatal(err.Error())
}
rows, err := db.Query("SELECT [ImageID],[AccommodationlID],[Link],[FileName] FROM [dbo].[AccomodationImage] where AccommodationlID between @from and @to and FileName not like '%NoHotel.png%'", sql.Named("from", from), sql.Named("to", to))
if err != nil {
log.Fatal(err.Error())
}
defer db.Close()
for rows.Next() {
var item = item{}
var accId, name string
_ = rows.Scan(&item.id, &accId, &item.url, &name)
item.path = fmt.Sprintf("%s\%s\%s", basePath, accId, name)
wg.Add(1)
go downloadFile(&item)
go func() {
select {
case done := <-ch:
wg.Add(1)
go saveAndUpdateFile(db, done)
case errorItem := <-errItems:
wg.Add(1)
go printResult(errorItem)
}
}()
}
wg.Wait()
fmt.Printf("%.2fs elapsedn", time.Since(start).Seconds())
}
func downloadFile(item *item) {
defer wg.Done()
resp, err := http.Get(item.url)
if err != nil {
item.content, item.err = nil, err
} else if resp.StatusCode != http.StatusOK {
item.content, item.err = nil, errors.New(resp.Status)
} else {
item.content, item.err = ioutil.ReadAll(resp.Body)
}
if item.err != nil {
errItems <- item
} else {
ch <- item
}
}
func saveAndUpdateFile(db *sql.DB, item *item) {
defer wg.Done()
if item.content == nil {
item.err = errors.New("Content is empty.")
} else {
dir := filepath.Dir(item.path)
err := os.MkdirAll(dir, os.ModePerm)
if err != nil {
item.err = err
} else {
item.err = ioutil.WriteFile(item.path, item.content, 0644)
}
}
if item.err == nil {
result, err := db.Exec("UPDATE [dbo].[AccomodationImage] SET IsRead = 1 WHERE ImageID = @id", sql.Named("id", item.id))
if rows, _ := result.RowsAffected(); rows <= 0 || err != nil {
item.err = errors.New("Update status failed.")
}
}
if item.err != nil {
errItems <- item
}
}
func printResult(item *item) {
defer wg.Done()
fmt.Println(item.toString())
}
I use a select statement to implement event so that when errItems or ch channels receive, save or print items. I also surrounded select statement with goroutine to prevent blocking.
sql go concurrency
New contributor
MatinMoezi is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.
$endgroup$
add a comment |
$begingroup$
I am new to Go and wrote program to download and save bulk URLs concurrently. It is working correctly, but I would like to make it more efficient and follow best practices.
package main
import (...)
// Connection String
const connectionString = "connectionString"
// Declare Channels
var wg sync.WaitGroup
var ch = make(chan *item) // For success item(s)
var errItems = make(chan *item) // For error item(s)
type item struct {
id int
url string
path string
content byte
err error
}
func main() {
start := time.Now()
var basePath = os.Args[1]
var from, to = os.Args[2], os.Args[3]
db, err := sql.Open("sqlserver", connectionString)
if err != nil {
log.Fatal(err.Error())
}
rows, err := db.Query("SELECT [ImageID],[AccommodationlID],[Link],[FileName] FROM [dbo].[AccomodationImage] where AccommodationlID between @from and @to and FileName not like '%NoHotel.png%'", sql.Named("from", from), sql.Named("to", to))
if err != nil {
log.Fatal(err.Error())
}
defer db.Close()
for rows.Next() {
var item = item{}
var accId, name string
_ = rows.Scan(&item.id, &accId, &item.url, &name)
item.path = fmt.Sprintf("%s\%s\%s", basePath, accId, name)
wg.Add(1)
go downloadFile(&item)
go func() {
select {
case done := <-ch:
wg.Add(1)
go saveAndUpdateFile(db, done)
case errorItem := <-errItems:
wg.Add(1)
go printResult(errorItem)
}
}()
}
wg.Wait()
fmt.Printf("%.2fs elapsedn", time.Since(start).Seconds())
}
func downloadFile(item *item) {
defer wg.Done()
resp, err := http.Get(item.url)
if err != nil {
item.content, item.err = nil, err
} else if resp.StatusCode != http.StatusOK {
item.content, item.err = nil, errors.New(resp.Status)
} else {
item.content, item.err = ioutil.ReadAll(resp.Body)
}
if item.err != nil {
errItems <- item
} else {
ch <- item
}
}
func saveAndUpdateFile(db *sql.DB, item *item) {
defer wg.Done()
if item.content == nil {
item.err = errors.New("Content is empty.")
} else {
dir := filepath.Dir(item.path)
err := os.MkdirAll(dir, os.ModePerm)
if err != nil {
item.err = err
} else {
item.err = ioutil.WriteFile(item.path, item.content, 0644)
}
}
if item.err == nil {
result, err := db.Exec("UPDATE [dbo].[AccomodationImage] SET IsRead = 1 WHERE ImageID = @id", sql.Named("id", item.id))
if rows, _ := result.RowsAffected(); rows <= 0 || err != nil {
item.err = errors.New("Update status failed.")
}
}
if item.err != nil {
errItems <- item
}
}
func printResult(item *item) {
defer wg.Done()
fmt.Println(item.toString())
}
I use a select statement to implement event so that when errItems or ch channels receive, save or print items. I also surrounded select statement with goroutine to prevent blocking.
sql go concurrency
New contributor
MatinMoezi is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.
$endgroup$
I am new to Go and wrote program to download and save bulk URLs concurrently. It is working correctly, but I would like to make it more efficient and follow best practices.
package main
import (...)
// Connection String
const connectionString = "connectionString"
// Declare Channels
var wg sync.WaitGroup
var ch = make(chan *item) // For success item(s)
var errItems = make(chan *item) // For error item(s)
type item struct {
id int
url string
path string
content byte
err error
}
func main() {
start := time.Now()
var basePath = os.Args[1]
var from, to = os.Args[2], os.Args[3]
db, err := sql.Open("sqlserver", connectionString)
if err != nil {
log.Fatal(err.Error())
}
rows, err := db.Query("SELECT [ImageID],[AccommodationlID],[Link],[FileName] FROM [dbo].[AccomodationImage] where AccommodationlID between @from and @to and FileName not like '%NoHotel.png%'", sql.Named("from", from), sql.Named("to", to))
if err != nil {
log.Fatal(err.Error())
}
defer db.Close()
for rows.Next() {
var item = item{}
var accId, name string
_ = rows.Scan(&item.id, &accId, &item.url, &name)
item.path = fmt.Sprintf("%s\%s\%s", basePath, accId, name)
wg.Add(1)
go downloadFile(&item)
go func() {
select {
case done := <-ch:
wg.Add(1)
go saveAndUpdateFile(db, done)
case errorItem := <-errItems:
wg.Add(1)
go printResult(errorItem)
}
}()
}
wg.Wait()
fmt.Printf("%.2fs elapsedn", time.Since(start).Seconds())
}
func downloadFile(item *item) {
defer wg.Done()
resp, err := http.Get(item.url)
if err != nil {
item.content, item.err = nil, err
} else if resp.StatusCode != http.StatusOK {
item.content, item.err = nil, errors.New(resp.Status)
} else {
item.content, item.err = ioutil.ReadAll(resp.Body)
}
if item.err != nil {
errItems <- item
} else {
ch <- item
}
}
func saveAndUpdateFile(db *sql.DB, item *item) {
defer wg.Done()
if item.content == nil {
item.err = errors.New("Content is empty.")
} else {
dir := filepath.Dir(item.path)
err := os.MkdirAll(dir, os.ModePerm)
if err != nil {
item.err = err
} else {
item.err = ioutil.WriteFile(item.path, item.content, 0644)
}
}
if item.err == nil {
result, err := db.Exec("UPDATE [dbo].[AccomodationImage] SET IsRead = 1 WHERE ImageID = @id", sql.Named("id", item.id))
if rows, _ := result.RowsAffected(); rows <= 0 || err != nil {
item.err = errors.New("Update status failed.")
}
}
if item.err != nil {
errItems <- item
}
}
func printResult(item *item) {
defer wg.Done()
fmt.Println(item.toString())
}
I use a select statement to implement event so that when errItems or ch channels receive, save or print items. I also surrounded select statement with goroutine to prevent blocking.
sql go concurrency
sql go concurrency
New contributor
MatinMoezi is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.
New contributor
MatinMoezi is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.
edited 4 hours ago
esote
2,7661937
2,7661937
New contributor
MatinMoezi is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.
asked 7 hours ago
MatinMoeziMatinMoezi
111
111
New contributor
MatinMoezi is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.
New contributor
MatinMoezi is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.
MatinMoezi is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.
add a comment |
add a comment |
1 Answer
1
active
oldest
votes
$begingroup$
Full code
import (...)
Please include your full code next time. This should instead be:
import (
"database/sql"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"path/filepath"
"sync"
"time"
)
You also have item.toString(), but this is nowhere defined. I changed it to:
fmt.Println(item)
Scope
connectionString doesn't need to be in the global scope, whether it's const or not. The same could be argued about wg, ch, and errItems -- but moving those to a lower scope would involve adding more function arguments, which is a choice you can make.
In terms of security, I recommend against hard-coding the connection string in the source code. Instead, as one of many options, you can have the connection credentials as a separate file that you read.
Simplify query
You use named arguments in your query. But here, the query is so short that it's clear from the context. You can split the query across multiple lines to make it easier to read. You also have some keywords in all uppercase and some in all lowercase.
q := `
SELECT [ImageID],
[AccommodationlID],
[Link],
[FileName]
FROM [dbo]. [AccomodationImage]
WHERE AccommodationlID BETWEEN ? AND ?
AND FileName NOT LIKE '%NoHotel.png%'`
rows, err := db.Query(q, from, to)
(Notice that you also have a misspelling in AccomodationImage.)
This kind of formatting for query strings is also how the Go documentation does it.
Vertical spacing
All of the code is compressed together and has no room to breathe. I recommend adding the occasional empty lines, such as between if-statements, for loops, goroutines, etc.
Validate os.Args
Your code assumes the program will always have three arguments. While this makes sense if only you plan to use it, it's generally not good practice.
Without bounds checking, you will get a runtime panic "index out of range" -- no a particularly useful message for those running the program.
if len(os.Args) < 4 {
log.Fatal("missing arguments")
}
You can also use short variable declaration when declaring basePath, from, and to.
basePath := os.Args[1]
from, to := os.Args[2], os.Args[3]
Default values
var item = item{}
This is redundant. You should be able to simply use var item -- please correct my if I'm wrong here and Rows.Scan() produces an error if item is nil. Since we do not need to initialize item, we can combine our declarations. Notice I rename the variable item to avoid confusion with the type item.
var (
i item
accID string
name string
)
Conclusion
Here's the code I ended up with:
package main
import (
"database/sql"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"path/filepath"
"sync"
"time"
)
type item struct {
id int
url string
path string
content byte
err error
}
var wg sync.WaitGroup
var ch = make(chan *item) // For success items
var errItems = make(chan *item) // For error items
func main() {
const connectionString = "connectionString"
if len(os.Args) < 4 {
log.Fatal("missing arguments")
}
start := time.Now()
basePath := os.Args[1]
from, to := os.Args[2], os.Args[3]
db, err := sql.Open("sqlserver", connectionString)
if err != nil {
log.Fatal(err.Error())
}
q := `
SELECT [ImageID],
[AccommodationlID],
[Link],
[FileName]
FROM [dbo]. [AccomodationImage]
WHERE AccommodationlID BETWEEN ? AND ?
AND FileName NOT LIKE '%NoHotel.png%'`
rows, err := db.Query(q, from, to)
if err != nil {
log.Fatal(err.Error())
}
defer db.Close()
for rows.Next() {
var (
i item
accID string
name string
)
_ = rows.Scan(&i.id, &accID, &i.url, &name)
i.path = fmt.Sprintf("%s\%s\%s", basePath, accID, name)
wg.Add(1)
go downloadFile(&i)
go func() {
select {
case done := <-ch:
wg.Add(1)
go saveAndUpdateFile(db, done)
case errorItem := <-errItems:
wg.Add(1)
go printResult(errorItem)
}
}()
}
wg.Wait()
fmt.Printf("%.2fs elapsedn", time.Since(start).Seconds())
}
func downloadFile(i *item) {
defer wg.Done()
resp, err := http.Get(i.url)
if err != nil {
i.content, i.err = nil, err
} else if resp.StatusCode != http.StatusOK {
i.content, i.err = nil, errors.New(resp.Status)
} else {
i.content, i.err = ioutil.ReadAll(resp.Body)
}
if i.err != nil {
errItems <- i
} else {
ch <- i
}
}
func saveAndUpdateFile(db *sql.DB, i *item) {
defer wg.Done()
if i.content == nil {
i.err = errors.New("Content is empty.")
} else {
dir := filepath.Dir(i.path)
err := os.MkdirAll(dir, os.ModePerm)
if err != nil {
i.err = err
} else {
i.err = ioutil.WriteFile(i.path, i.content, 0644)
}
}
q := `
UPDATE [dbo].[AccomodationImage]
SET IsRead = 1
WHERE ImageID = ?`
if i.err == nil {
result, err := db.Exec(q, i.id)
if rows, _ := result.RowsAffected(); rows <= 0 || err != nil {
i.err = errors.New("Update status failed.")
}
}
if i.err != nil {
errItems <- i
}
}
func printResult(i *item) {
defer wg.Done()
fmt.Println(i)
}
Unfortunately I cannot test this code and suggest algorithmic or more in-depth changes.
Hope this helps!
$endgroup$
add a comment |
Your Answer
StackExchange.ifUsing("editor", function () {
return StackExchange.using("mathjaxEditing", function () {
StackExchange.MarkdownEditor.creationCallbacks.add(function (editor, postfix) {
StackExchange.mathjaxEditing.prepareWmdForMathJax(editor, postfix, [["\$", "\$"]]);
});
});
}, "mathjax-editing");
StackExchange.ifUsing("editor", function () {
StackExchange.using("externalEditor", function () {
StackExchange.using("snippets", function () {
StackExchange.snippets.init();
});
});
}, "code-snippets");
StackExchange.ready(function() {
var channelOptions = {
tags: "".split(" "),
id: "196"
};
initTagRenderer("".split(" "), "".split(" "), channelOptions);
StackExchange.using("externalEditor", function() {
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled) {
StackExchange.using("snippets", function() {
createEditor();
});
}
else {
createEditor();
}
});
function createEditor() {
StackExchange.prepareEditor({
heartbeatType: 'answer',
autoActivateHeartbeat: false,
convertImagesToLinks: false,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: null,
bindNavPrevention: true,
postfix: "",
imageUploader: {
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
},
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
});
}
});
MatinMoezi is a new contributor. Be nice, and check out our Code of Conduct.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f215433%2fdownload-and-save-bulk-url-concurrently%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
1 Answer
1
active
oldest
votes
1 Answer
1
active
oldest
votes
active
oldest
votes
active
oldest
votes
$begingroup$
Full code
import (...)
Please include your full code next time. This should instead be:
import (
"database/sql"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"path/filepath"
"sync"
"time"
)
You also have item.toString(), but this is nowhere defined. I changed it to:
fmt.Println(item)
Scope
connectionString doesn't need to be in the global scope, whether it's const or not. The same could be argued about wg, ch, and errItems -- but moving those to a lower scope would involve adding more function arguments, which is a choice you can make.
In terms of security, I recommend against hard-coding the connection string in the source code. Instead, as one of many options, you can have the connection credentials as a separate file that you read.
Simplify query
You use named arguments in your query. But here, the query is so short that it's clear from the context. You can split the query across multiple lines to make it easier to read. You also have some keywords in all uppercase and some in all lowercase.
q := `
SELECT [ImageID],
[AccommodationlID],
[Link],
[FileName]
FROM [dbo]. [AccomodationImage]
WHERE AccommodationlID BETWEEN ? AND ?
AND FileName NOT LIKE '%NoHotel.png%'`
rows, err := db.Query(q, from, to)
(Notice that you also have a misspelling in AccomodationImage.)
This kind of formatting for query strings is also how the Go documentation does it.
Vertical spacing
All of the code is compressed together and has no room to breathe. I recommend adding the occasional empty lines, such as between if-statements, for loops, goroutines, etc.
Validate os.Args
Your code assumes the program will always have three arguments. While this makes sense if only you plan to use it, it's generally not good practice.
Without bounds checking, you will get a runtime panic "index out of range" -- no a particularly useful message for those running the program.
if len(os.Args) < 4 {
log.Fatal("missing arguments")
}
You can also use short variable declaration when declaring basePath, from, and to.
basePath := os.Args[1]
from, to := os.Args[2], os.Args[3]
Default values
var item = item{}
This is redundant. You should be able to simply use var item -- please correct my if I'm wrong here and Rows.Scan() produces an error if item is nil. Since we do not need to initialize item, we can combine our declarations. Notice I rename the variable item to avoid confusion with the type item.
var (
i item
accID string
name string
)
Conclusion
Here's the code I ended up with:
package main
import (
"database/sql"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"path/filepath"
"sync"
"time"
)
type item struct {
id int
url string
path string
content byte
err error
}
var wg sync.WaitGroup
var ch = make(chan *item) // For success items
var errItems = make(chan *item) // For error items
func main() {
const connectionString = "connectionString"
if len(os.Args) < 4 {
log.Fatal("missing arguments")
}
start := time.Now()
basePath := os.Args[1]
from, to := os.Args[2], os.Args[3]
db, err := sql.Open("sqlserver", connectionString)
if err != nil {
log.Fatal(err.Error())
}
q := `
SELECT [ImageID],
[AccommodationlID],
[Link],
[FileName]
FROM [dbo]. [AccomodationImage]
WHERE AccommodationlID BETWEEN ? AND ?
AND FileName NOT LIKE '%NoHotel.png%'`
rows, err := db.Query(q, from, to)
if err != nil {
log.Fatal(err.Error())
}
defer db.Close()
for rows.Next() {
var (
i item
accID string
name string
)
_ = rows.Scan(&i.id, &accID, &i.url, &name)
i.path = fmt.Sprintf("%s\%s\%s", basePath, accID, name)
wg.Add(1)
go downloadFile(&i)
go func() {
select {
case done := <-ch:
wg.Add(1)
go saveAndUpdateFile(db, done)
case errorItem := <-errItems:
wg.Add(1)
go printResult(errorItem)
}
}()
}
wg.Wait()
fmt.Printf("%.2fs elapsedn", time.Since(start).Seconds())
}
func downloadFile(i *item) {
defer wg.Done()
resp, err := http.Get(i.url)
if err != nil {
i.content, i.err = nil, err
} else if resp.StatusCode != http.StatusOK {
i.content, i.err = nil, errors.New(resp.Status)
} else {
i.content, i.err = ioutil.ReadAll(resp.Body)
}
if i.err != nil {
errItems <- i
} else {
ch <- i
}
}
func saveAndUpdateFile(db *sql.DB, i *item) {
defer wg.Done()
if i.content == nil {
i.err = errors.New("Content is empty.")
} else {
dir := filepath.Dir(i.path)
err := os.MkdirAll(dir, os.ModePerm)
if err != nil {
i.err = err
} else {
i.err = ioutil.WriteFile(i.path, i.content, 0644)
}
}
q := `
UPDATE [dbo].[AccomodationImage]
SET IsRead = 1
WHERE ImageID = ?`
if i.err == nil {
result, err := db.Exec(q, i.id)
if rows, _ := result.RowsAffected(); rows <= 0 || err != nil {
i.err = errors.New("Update status failed.")
}
}
if i.err != nil {
errItems <- i
}
}
func printResult(i *item) {
defer wg.Done()
fmt.Println(i)
}
Unfortunately I cannot test this code and suggest algorithmic or more in-depth changes.
Hope this helps!
$endgroup$
add a comment |
$begingroup$
Full code
import (...)
Please include your full code next time. This should instead be:
import (
"database/sql"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"path/filepath"
"sync"
"time"
)
You also have item.toString(), but this is nowhere defined. I changed it to:
fmt.Println(item)
Scope
connectionString doesn't need to be in the global scope, whether it's const or not. The same could be argued about wg, ch, and errItems -- but moving those to a lower scope would involve adding more function arguments, which is a choice you can make.
In terms of security, I recommend against hard-coding the connection string in the source code. Instead, as one of many options, you can have the connection credentials as a separate file that you read.
Simplify query
You use named arguments in your query. But here, the query is so short that it's clear from the context. You can split the query across multiple lines to make it easier to read. You also have some keywords in all uppercase and some in all lowercase.
q := `
SELECT [ImageID],
[AccommodationlID],
[Link],
[FileName]
FROM [dbo]. [AccomodationImage]
WHERE AccommodationlID BETWEEN ? AND ?
AND FileName NOT LIKE '%NoHotel.png%'`
rows, err := db.Query(q, from, to)
(Notice that you also have a misspelling in AccomodationImage.)
This kind of formatting for query strings is also how the Go documentation does it.
Vertical spacing
All of the code is compressed together and has no room to breathe. I recommend adding the occasional empty lines, such as between if-statements, for loops, goroutines, etc.
Validate os.Args
Your code assumes the program will always have three arguments. While this makes sense if only you plan to use it, it's generally not good practice.
Without bounds checking, you will get a runtime panic "index out of range" -- no a particularly useful message for those running the program.
if len(os.Args) < 4 {
log.Fatal("missing arguments")
}
You can also use short variable declaration when declaring basePath, from, and to.
basePath := os.Args[1]
from, to := os.Args[2], os.Args[3]
Default values
var item = item{}
This is redundant. You should be able to simply use var item -- please correct my if I'm wrong here and Rows.Scan() produces an error if item is nil. Since we do not need to initialize item, we can combine our declarations. Notice I rename the variable item to avoid confusion with the type item.
var (
i item
accID string
name string
)
Conclusion
Here's the code I ended up with:
package main
import (
"database/sql"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"path/filepath"
"sync"
"time"
)
type item struct {
id int
url string
path string
content byte
err error
}
var wg sync.WaitGroup
var ch = make(chan *item) // For success items
var errItems = make(chan *item) // For error items
func main() {
const connectionString = "connectionString"
if len(os.Args) < 4 {
log.Fatal("missing arguments")
}
start := time.Now()
basePath := os.Args[1]
from, to := os.Args[2], os.Args[3]
db, err := sql.Open("sqlserver", connectionString)
if err != nil {
log.Fatal(err.Error())
}
q := `
SELECT [ImageID],
[AccommodationlID],
[Link],
[FileName]
FROM [dbo]. [AccomodationImage]
WHERE AccommodationlID BETWEEN ? AND ?
AND FileName NOT LIKE '%NoHotel.png%'`
rows, err := db.Query(q, from, to)
if err != nil {
log.Fatal(err.Error())
}
defer db.Close()
for rows.Next() {
var (
i item
accID string
name string
)
_ = rows.Scan(&i.id, &accID, &i.url, &name)
i.path = fmt.Sprintf("%s\%s\%s", basePath, accID, name)
wg.Add(1)
go downloadFile(&i)
go func() {
select {
case done := <-ch:
wg.Add(1)
go saveAndUpdateFile(db, done)
case errorItem := <-errItems:
wg.Add(1)
go printResult(errorItem)
}
}()
}
wg.Wait()
fmt.Printf("%.2fs elapsedn", time.Since(start).Seconds())
}
func downloadFile(i *item) {
defer wg.Done()
resp, err := http.Get(i.url)
if err != nil {
i.content, i.err = nil, err
} else if resp.StatusCode != http.StatusOK {
i.content, i.err = nil, errors.New(resp.Status)
} else {
i.content, i.err = ioutil.ReadAll(resp.Body)
}
if i.err != nil {
errItems <- i
} else {
ch <- i
}
}
func saveAndUpdateFile(db *sql.DB, i *item) {
defer wg.Done()
if i.content == nil {
i.err = errors.New("Content is empty.")
} else {
dir := filepath.Dir(i.path)
err := os.MkdirAll(dir, os.ModePerm)
if err != nil {
i.err = err
} else {
i.err = ioutil.WriteFile(i.path, i.content, 0644)
}
}
q := `
UPDATE [dbo].[AccomodationImage]
SET IsRead = 1
WHERE ImageID = ?`
if i.err == nil {
result, err := db.Exec(q, i.id)
if rows, _ := result.RowsAffected(); rows <= 0 || err != nil {
i.err = errors.New("Update status failed.")
}
}
if i.err != nil {
errItems <- i
}
}
func printResult(i *item) {
defer wg.Done()
fmt.Println(i)
}
Unfortunately I cannot test this code and suggest algorithmic or more in-depth changes.
Hope this helps!
$endgroup$
add a comment |
$begingroup$
Full code
import (...)
Please include your full code next time. This should instead be:
import (
"database/sql"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"path/filepath"
"sync"
"time"
)
You also have item.toString(), but this is nowhere defined. I changed it to:
fmt.Println(item)
Scope
connectionString doesn't need to be in the global scope, whether it's const or not. The same could be argued about wg, ch, and errItems -- but moving those to a lower scope would involve adding more function arguments, which is a choice you can make.
In terms of security, I recommend against hard-coding the connection string in the source code. Instead, as one of many options, you can have the connection credentials as a separate file that you read.
Simplify query
You use named arguments in your query. But here, the query is so short that it's clear from the context. You can split the query across multiple lines to make it easier to read. You also have some keywords in all uppercase and some in all lowercase.
q := `
SELECT [ImageID],
[AccommodationlID],
[Link],
[FileName]
FROM [dbo]. [AccomodationImage]
WHERE AccommodationlID BETWEEN ? AND ?
AND FileName NOT LIKE '%NoHotel.png%'`
rows, err := db.Query(q, from, to)
(Notice that you also have a misspelling in AccomodationImage.)
This kind of formatting for query strings is also how the Go documentation does it.
Vertical spacing
All of the code is compressed together and has no room to breathe. I recommend adding the occasional empty lines, such as between if-statements, for loops, goroutines, etc.
Validate os.Args
Your code assumes the program will always have three arguments. While this makes sense if only you plan to use it, it's generally not good practice.
Without bounds checking, you will get a runtime panic "index out of range" -- no a particularly useful message for those running the program.
if len(os.Args) < 4 {
log.Fatal("missing arguments")
}
You can also use short variable declaration when declaring basePath, from, and to.
basePath := os.Args[1]
from, to := os.Args[2], os.Args[3]
Default values
var item = item{}
This is redundant. You should be able to simply use var item -- please correct my if I'm wrong here and Rows.Scan() produces an error if item is nil. Since we do not need to initialize item, we can combine our declarations. Notice I rename the variable item to avoid confusion with the type item.
var (
i item
accID string
name string
)
Conclusion
Here's the code I ended up with:
package main
import (
"database/sql"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"path/filepath"
"sync"
"time"
)
type item struct {
id int
url string
path string
content byte
err error
}
var wg sync.WaitGroup
var ch = make(chan *item) // For success items
var errItems = make(chan *item) // For error items
func main() {
const connectionString = "connectionString"
if len(os.Args) < 4 {
log.Fatal("missing arguments")
}
start := time.Now()
basePath := os.Args[1]
from, to := os.Args[2], os.Args[3]
db, err := sql.Open("sqlserver", connectionString)
if err != nil {
log.Fatal(err.Error())
}
q := `
SELECT [ImageID],
[AccommodationlID],
[Link],
[FileName]
FROM [dbo]. [AccomodationImage]
WHERE AccommodationlID BETWEEN ? AND ?
AND FileName NOT LIKE '%NoHotel.png%'`
rows, err := db.Query(q, from, to)
if err != nil {
log.Fatal(err.Error())
}
defer db.Close()
for rows.Next() {
var (
i item
accID string
name string
)
_ = rows.Scan(&i.id, &accID, &i.url, &name)
i.path = fmt.Sprintf("%s\%s\%s", basePath, accID, name)
wg.Add(1)
go downloadFile(&i)
go func() {
select {
case done := <-ch:
wg.Add(1)
go saveAndUpdateFile(db, done)
case errorItem := <-errItems:
wg.Add(1)
go printResult(errorItem)
}
}()
}
wg.Wait()
fmt.Printf("%.2fs elapsedn", time.Since(start).Seconds())
}
func downloadFile(i *item) {
defer wg.Done()
resp, err := http.Get(i.url)
if err != nil {
i.content, i.err = nil, err
} else if resp.StatusCode != http.StatusOK {
i.content, i.err = nil, errors.New(resp.Status)
} else {
i.content, i.err = ioutil.ReadAll(resp.Body)
}
if i.err != nil {
errItems <- i
} else {
ch <- i
}
}
func saveAndUpdateFile(db *sql.DB, i *item) {
defer wg.Done()
if i.content == nil {
i.err = errors.New("Content is empty.")
} else {
dir := filepath.Dir(i.path)
err := os.MkdirAll(dir, os.ModePerm)
if err != nil {
i.err = err
} else {
i.err = ioutil.WriteFile(i.path, i.content, 0644)
}
}
q := `
UPDATE [dbo].[AccomodationImage]
SET IsRead = 1
WHERE ImageID = ?`
if i.err == nil {
result, err := db.Exec(q, i.id)
if rows, _ := result.RowsAffected(); rows <= 0 || err != nil {
i.err = errors.New("Update status failed.")
}
}
if i.err != nil {
errItems <- i
}
}
func printResult(i *item) {
defer wg.Done()
fmt.Println(i)
}
Unfortunately I cannot test this code and suggest algorithmic or more in-depth changes.
Hope this helps!
$endgroup$
Full code
import (...)
Please include your full code next time. This should instead be:
import (
"database/sql"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"path/filepath"
"sync"
"time"
)
You also have item.toString(), but this is nowhere defined. I changed it to:
fmt.Println(item)
Scope
connectionString doesn't need to be in the global scope, whether it's const or not. The same could be argued about wg, ch, and errItems -- but moving those to a lower scope would involve adding more function arguments, which is a choice you can make.
In terms of security, I recommend against hard-coding the connection string in the source code. Instead, as one of many options, you can have the connection credentials as a separate file that you read.
Simplify query
You use named arguments in your query. But here, the query is so short that it's clear from the context. You can split the query across multiple lines to make it easier to read. You also have some keywords in all uppercase and some in all lowercase.
q := `
SELECT [ImageID],
[AccommodationlID],
[Link],
[FileName]
FROM [dbo]. [AccomodationImage]
WHERE AccommodationlID BETWEEN ? AND ?
AND FileName NOT LIKE '%NoHotel.png%'`
rows, err := db.Query(q, from, to)
(Notice that you also have a misspelling in AccomodationImage.)
This kind of formatting for query strings is also how the Go documentation does it.
Vertical spacing
All of the code is compressed together and has no room to breathe. I recommend adding the occasional empty lines, such as between if-statements, for loops, goroutines, etc.
Validate os.Args
Your code assumes the program will always have three arguments. While this makes sense if only you plan to use it, it's generally not good practice.
Without bounds checking, you will get a runtime panic "index out of range" -- no a particularly useful message for those running the program.
if len(os.Args) < 4 {
log.Fatal("missing arguments")
}
You can also use short variable declaration when declaring basePath, from, and to.
basePath := os.Args[1]
from, to := os.Args[2], os.Args[3]
Default values
var item = item{}
This is redundant. You should be able to simply use var item -- please correct my if I'm wrong here and Rows.Scan() produces an error if item is nil. Since we do not need to initialize item, we can combine our declarations. Notice I rename the variable item to avoid confusion with the type item.
var (
i item
accID string
name string
)
Conclusion
Here's the code I ended up with:
package main
import (
"database/sql"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"path/filepath"
"sync"
"time"
)
type item struct {
id int
url string
path string
content byte
err error
}
var wg sync.WaitGroup
var ch = make(chan *item) // For success items
var errItems = make(chan *item) // For error items
func main() {
const connectionString = "connectionString"
if len(os.Args) < 4 {
log.Fatal("missing arguments")
}
start := time.Now()
basePath := os.Args[1]
from, to := os.Args[2], os.Args[3]
db, err := sql.Open("sqlserver", connectionString)
if err != nil {
log.Fatal(err.Error())
}
q := `
SELECT [ImageID],
[AccommodationlID],
[Link],
[FileName]
FROM [dbo]. [AccomodationImage]
WHERE AccommodationlID BETWEEN ? AND ?
AND FileName NOT LIKE '%NoHotel.png%'`
rows, err := db.Query(q, from, to)
if err != nil {
log.Fatal(err.Error())
}
defer db.Close()
for rows.Next() {
var (
i item
accID string
name string
)
_ = rows.Scan(&i.id, &accID, &i.url, &name)
i.path = fmt.Sprintf("%s\%s\%s", basePath, accID, name)
wg.Add(1)
go downloadFile(&i)
go func() {
select {
case done := <-ch:
wg.Add(1)
go saveAndUpdateFile(db, done)
case errorItem := <-errItems:
wg.Add(1)
go printResult(errorItem)
}
}()
}
wg.Wait()
fmt.Printf("%.2fs elapsedn", time.Since(start).Seconds())
}
func downloadFile(i *item) {
defer wg.Done()
resp, err := http.Get(i.url)
if err != nil {
i.content, i.err = nil, err
} else if resp.StatusCode != http.StatusOK {
i.content, i.err = nil, errors.New(resp.Status)
} else {
i.content, i.err = ioutil.ReadAll(resp.Body)
}
if i.err != nil {
errItems <- i
} else {
ch <- i
}
}
func saveAndUpdateFile(db *sql.DB, i *item) {
defer wg.Done()
if i.content == nil {
i.err = errors.New("Content is empty.")
} else {
dir := filepath.Dir(i.path)
err := os.MkdirAll(dir, os.ModePerm)
if err != nil {
i.err = err
} else {
i.err = ioutil.WriteFile(i.path, i.content, 0644)
}
}
q := `
UPDATE [dbo].[AccomodationImage]
SET IsRead = 1
WHERE ImageID = ?`
if i.err == nil {
result, err := db.Exec(q, i.id)
if rows, _ := result.RowsAffected(); rows <= 0 || err != nil {
i.err = errors.New("Update status failed.")
}
}
if i.err != nil {
errItems <- i
}
}
func printResult(i *item) {
defer wg.Done()
fmt.Println(i)
}
Unfortunately I cannot test this code and suggest algorithmic or more in-depth changes.
Hope this helps!
edited 4 hours ago
answered 4 hours ago
esoteesote
2,7661937
2,7661937
add a comment |
add a comment |
MatinMoezi is a new contributor. Be nice, and check out our Code of Conduct.
MatinMoezi is a new contributor. Be nice, and check out our Code of Conduct.
MatinMoezi is a new contributor. Be nice, and check out our Code of Conduct.
MatinMoezi is a new contributor. Be nice, and check out our Code of Conduct.
Thanks for contributing an answer to Code Review Stack Exchange!
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
Use MathJax to format equations. MathJax reference.
To learn more, see our tips on writing great answers.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f215433%2fdownload-and-save-bulk-url-concurrently%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown